[groheondus] Fix missing automatic refresh ++ (#11955)

* Fix scheduling of thing and token update, tries to avoid service rate limiting. Added more logging. Added some missing null checks. Ensure recent data is fetched, not data from yesterday

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Updated to latest versio of API lib

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Added new channel waterconsumption_since_midnight that sums todays water consumption (same as in the Grohe app)

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Add more debug logging

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* More null checks, also set channels to Undef if a value is missing

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Fixed missing embedding of commons-text as it is a dependency of the api lib

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Refresh token 1 hour before expiry

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Re-login in case token refresh fails

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Factor in timezone when calculating consum since midnight

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Use QuantityType<Volume> for water consumption

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Minor

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* i18n of dynamic error messages

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* More i18n. Plus retry of failed refresh token - with a delay to possibly avoid rate limiting

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Adjust refresh token timeout to 5 minutes before expire. Also retry with username/pwd login if token login fails (could be an expired token)

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Clear old discovery results

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Fetch data further back to ensure battery device has been online

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Updated README with old data warning

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Typo

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Do not allow polling interval less than 900 as rate limiting most likely will block the calls

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Fix failed token refresh giving up

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Removed refresh token login webpage. Another attempt at handling token refresh

Signed-off-by: Arne Seime <arne.seime@gmail.com>

* Fix status detail

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Restore formatting

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Fix newly introduced warnings

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Remove redundant logging

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

Signed-off-by: Arne Seime <arne.seime@gmail.com>
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
Co-authored-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Arne Seime 2022-10-27 09:03:17 +02:00 committed by GitHub
parent 26ad08cca9
commit 618b7a4e3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 393 additions and 600 deletions

View File

@ -5,7 +5,7 @@ The binding uses the REST API interface (the same as used by the Android App) to
## Supported Things
This binding should support all appliances from GROHE, however, only the GROHE Sense Guard is tested with it.
This binding should support all appliances from GROHE, however, only the GROHE Sense and Sense Guard is tested with it.
| Thing type | Name |
|--------------------------|--------------------------|
@ -29,18 +29,15 @@ There is only one thing and one bridge that needs to be configured together to g
### Account Bridge
The `groheondus:account` bridge is used to configure the API interface for a specific account, which is used to access the collected and saved data of your GROHE account.
You can either use your username and password combination for logging in into your GROHE account, in which case both parameters, `username` as well as `password`, are required arguments and refer to the same login credentials you used during setting up your GROHE account or while logging into the app.
Alternatively you can use a so called `refresh token` to grant openHAB access to your account without having to share your credentials with the system.
For that you need to obtain such `refresh token` from the GROHE ONDUS Api (see more on that below) and paste this string into the respective input field on the account management page you can reach from `http://<your-openHAB-domain-and-port>/groheondus`.
On this site you can also delete a previously saved `refresh token`.
The GROHE ONDUS binding also refreshes this refresh token in order to ensure that you stay logged in.
Use the same credentials as in the mobile app.
### Appliance
The `groheondus:sense` and `groheondus:senseguard` things are used to retrieve information of a specific appliance from GROHE.
This appliance needs to be connected with your GROHE ONDUS account as configured in the corresponding Account Bridge.
The appliance needs to be configured with the unique appliance ID (with the `applianceId` configuration) as well as the `roomId`
and the `locationId`. Once the account bridge is configured, the appliances in your account will be discovered as Appliance things.
The appliance needs to be configured with the unique appliance ID (with the `applianceId` configuration) as well as the `roomId` and the `locationId`.
Once the account bridge is configured, the appliances in your account will be discovered as Appliance things.
`pollingInterval` has a minimum value of 900 seconds to avoid service rate limiting.
| Configuration | Default value | Description |
|--------------------------|--------------------------|-------------------------------------------------------|
@ -48,19 +45,20 @@ and the `locationId`. Once the account bridge is configured, the appliances in y
| roomId | '' | ID of the room the appliance is in |
| locationId | '' | ID of the location (building) the appliance is in |
| pollingInterval | Retrieved from API, | Interval in seconds to get new data from the API |
| | usually 900 | The `sense` thing uses 900 by default |
| | usually 900 | The `sense` thing uses 900 by default. |
#### Channels
##### senseguard
| Channel | Type | Description |
|--------------------------|--------------------------|-------------------------------------------------------|
| name | String | The name of the appliance |
| pressure | Number:Pressure | The pressure of your water supply |
| temperature_guard | Number:Temperature | The ambient temperature of the appliance |
| valve_open | Switch | Valve switch |
| waterconsumption | Number | The amount of water used in a specific timeframe |
| Channel | Type | Description |
|---------------------------------|--------------------|--------------------------------------------------|
| name | String | The name of the appliance |
| pressure | Number:Pressure | The pressure of your water supply |
| temperature_guard | Number:Temperature | The ambient temperature of the appliance |
| valve_open | Switch | Valve switch |
| waterconsumption | Number:Volume | The amount of water used in a specific timeframe |
| waterconsumption_since_midnight | Number:Volume | The amount of water used since midnight |
##### sense
@ -71,6 +69,8 @@ and the `locationId`. Once the account bridge is configured, the appliances in y
| temperature | Number:Temperature | The ambient temperature of the appliance |
| battery | Number | The battery level of the appliance |
Note: Be aware that the Sense reports data once a day (at most), and that the value posted in the channel - however the latest - may be up to 48 hours old.
## Full Example
Things file:
@ -93,47 +93,9 @@ Items file:
String Name_Sense_Guard "Appliance Name" {channel="groheondus:senseguard:groheondus:appliance:550e8400-e29b-11d4-a716-446655440000:name"}
Number:Pressure Pressure_Sense_Guard "Pressure [%.1f %unit%]" {channel="groheondus:senseguard:groheondus:appliance:550e8400-e29b-11d4-a716-446655440000:pressure"}
Number:Temperature Temperature_Sense_Guard "Temperature [%.1f %unit%]" {channel="groheondus:senseguard:groheondus:appliance:550e8400-e29b-11d4-a716-446655440000:temperature_guard"}
Number:Volume Water_Usage_Since_Midnight_Sense_Guard "Water usage since midnight [%.1f %unit%]" {channel="groheondus:senseguard:groheondus:appliance:550e8400-e29b-11d4-a716-446655440000:waterconsumption_since_midnight"}
String Name_Sense "Temperature [%.1f %unit%]" {channel="groheondus:sense:groheondus:appliance:444e8400-e29b-11d4-a716-446655440000:name"}
Number:Temperature Temperature_Sense "Temperature [%.1f %unit%]" {channel="groheondus:sense:groheondus:appliance:444e8400-e29b-11d4-a716-446655440000:temperature"}
Number Humidity_Sense "Humidity [%.1f %unit%]" {channel="groheondus:sense:groheondus:appliance:444e8400-e29b-11d4-a716-446655440000:humidity"}
````
## Obtaining a `refresh token`
Actually obtaining a `refresh token` from the GROHE ONDUS Api requires some manual steps.
In order to more deeply understand what is happening during the process, you can read more information about the OAuth2/OIDC (OpenID Connect) login flow by searching for these terms in your favorite search engine.
Here is a short step-by-step guide on how to obtain a refresh token:
1. Open a new tab in your Internet browser
2. Open the developer console of your browser (mostly possible by pressing F12)
3. Select the network tab of the developer console (which shows you the network request done by the browser)
4. Open the following URL: https://idp2-apigw.cloud.grohe.com/v3/iot/oidc/login
5. You will automatically being redirected to the GROHE ONDUS login page, login there
6. After logging in successfully, nothing should happen, except a failed request to a page starting with `token?`
7. Click on this request (the URL in the request overview should start with `ondus://idp2-apigw.cloud.grohe.com/v3/iot/oidc/token?` or something like that
8. Copy the whole request URL (which should contain a lot of stuff, like a `state` parameter and so on)
9. Open a new tab in your Internet browser and paste the URL into the address bar (do not hit ENTER or start the navigation to this page, yet)
10. Replace the `ondus://` part of the URL with `https://` and hit ENTER
11. The response of the page should be plain text with a so called `JSON object`. Somewhere in the text should be a `refresh_token` string, select the string after this `refresh_token` text, which is encapsulated with `"`.
E.g.: If the response of the page looks like this:
````
{
"access_token": "the_access_token",
"expires_in":3600,
"refresh_expires_in":15552000,
"refresh_token":"the_refresh_token",
"token_type":"bearer",
"id_token":"the_id_token",
"not-before-policy":0,
"session_state":"a-state",
"scope":"",
"tandc_accepted":true,
"partialLogin":false
}
````
Then the `refresh_token` value you should copy would be: `the_refresh_token`.
This value is the `refresh token` you should save as described above.

View File

@ -15,14 +15,14 @@
<name>openHAB Add-ons :: Bundles :: GROHE ONDUS Binding</name>
<properties>
<dep.noembedding>commons-text,commons-lang3</dep.noembedding>
<dep.noembedding>commons-lang3</dep.noembedding>
</properties>
<dependencies>
<dependency>
<groupId>org.grohe</groupId>
<groupId>io.github.floriansw</groupId>
<artifactId>ondus-api</artifactId>
<version>1.0.0</version>
<version>2.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -4,9 +4,9 @@
<feature name="openhab-binding-groheondus" description="GROHE ONDUS Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature prerequisite="true">wrap</feature>
<feature dependency="true">openhab.tp-jackson</feature>
<bundle dependency="true">mvn:org.apache.commons/commons-text/1.6</bundle>
<bundle dependency="true">mvn:org.apache.commons/commons-lang3/3.8.1</bundle>
<bundle dependency="true">wrap:mvn:org.apache.commons/commons-text/1.6</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.groheondus/${project.version}</bundle>
</feature>
</features>

View File

@ -1,134 +0,0 @@
/**
* Copyright (c) 2010-2022 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.groheondus.internal;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.groheondus.internal.handler.GroheOndusAccountHandler;
import org.osgi.service.http.HttpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Florian Schmidt - Initial contribution
*/
@NonNullByDefault
public class AccountServlet extends HttpServlet {
private static final long serialVersionUID = -6321196284331950479L;
private final Logger logger = LoggerFactory.getLogger(AccountServlet.class);
private HttpService httpService;
private String bridgeId;
private GroheOndusAccountHandler accountHandler;
public AccountServlet(HttpService httpService, String bridgeId, GroheOndusAccountHandler accountHandler) {
this.httpService = httpService;
this.bridgeId = bridgeId;
this.accountHandler = accountHandler;
try {
httpService.registerServlet(servletUrl(), this, null, httpService.createDefaultHttpContext());
} catch (Exception e) {
logger.warn("Register servlet fails", e);
}
}
private String servletUrl() {
return "/groheondus/" + URLEncoder.encode(bridgeId, StandardCharsets.UTF_8);
}
@Override
protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
throws ServletException, IOException {
if (req == null || resp == null) {
return;
}
resp.addHeader("content-type", "text/html;charset=UTF-8");
StringBuilder htmlString = new StringBuilder();
htmlString.append("<html>");
htmlString.append("<head>");
htmlString.append("<title>Set refresh token</title>");
htmlString.append("</head>");
htmlString.append("<body>");
htmlString.append("<header>");
htmlString.append("<h1>Set refresh token for accout: ");
htmlString.append(bridgeId);
htmlString.append("</h1>");
htmlString.append("</header>");
htmlString.append("<div>Has refresh token: ");
if (this.accountHandler.hasRefreshToken()) {
htmlString.append("yes");
htmlString.append(
"<input type=\"submit\" value=\"Delete\" onclick=\"fetch(window.location.href, {method: 'DELETE'}).then(window.location.reload())\">");
} else {
htmlString.append("no");
}
htmlString.append("</div>");
htmlString.append("<form method=\"post\">");
htmlString.append("<label for=\"refreshToken\">Refresh Token: </label>");
htmlString.append("<input type=\"text\" id=\"refreshToken\" autocomplete=\"off\" name=\"refreshToken\">");
htmlString.append("<input type=\"submit\" value=\"Save\">");
htmlString.append("</form>");
htmlString.append("</body>");
htmlString.append("</html>");
resp.getWriter().write(htmlString.toString());
}
@Override
protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
throws ServletException, IOException {
if (req == null) {
return;
}
if (resp == null) {
return;
}
Map<String, String[]> map = req.getParameterMap();
this.accountHandler.setRefreshToken(map.get("refreshToken")[0]);
resp.addHeader("Location", "/groheondus");
resp.setStatus(HttpStatus.MOVED_TEMPORARILY_302);
}
@Override
protected void doDelete(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
throws ServletException, IOException {
if (req == null) {
return;
}
if (resp == null) {
return;
}
this.accountHandler.deleteRefreshToken();
resp.setStatus(HttpStatus.OK_200);
}
public void dispose() {
httpService.unregister(servletUrl());
}
}

View File

@ -1,114 +0,0 @@
/**
* Copyright (c) 2010-2022 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.groheondus.internal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.core.thing.Thing;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ServiceScope;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Florian Schmidt - Initial contribution
*/
@NonNullByDefault
@Component(service = AccountsServlet.class, scope = ServiceScope.SINGLETON)
public class AccountsServlet extends HttpServlet {
private static final long serialVersionUID = -9183159739446995608L;
private static final String SERVLET_URL = "/groheondus";
private final Logger logger = LoggerFactory.getLogger(AccountsServlet.class);
private final List<Thing> accounts = new ArrayList<>();
private HttpService httpService;
@Activate
public AccountsServlet(@Reference HttpService httpService) {
this.httpService = httpService;
try {
httpService.registerServlet(SERVLET_URL, this, null, httpService.createDefaultHttpContext());
} catch (ServletException | NamespaceException e) {
logger.warn("Register servlet fails", e);
}
}
public void addAccount(Thing accountThing) {
accounts.add(accountThing);
}
public void removeAccount(Thing accountThing) {
accounts.remove(accountThing);
}
public void deactivate() {
httpService.unregister(SERVLET_URL);
}
@Override
protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
throws ServletException, IOException {
if (req == null || resp == null) {
return;
}
StringBuilder htmlString = new StringBuilder();
htmlString.append("<html>");
htmlString.append("<head>");
htmlString.append("<title>GROHE Ondus Account login</title>");
htmlString.append("</head>");
htmlString.append("<body>");
if (accounts.isEmpty()) {
htmlString.append(
"Please first create an GROHE ONDUS account thing in openHAB in order to log into this account.");
} else {
htmlString.append(
"You've the following GROHE ONDUS account things, click on the one you want to manage:<br />");
htmlString.append("<ul>");
accounts.forEach(account -> {
String accountId = account.getUID().getId();
htmlString.append("<li>");
htmlString.append("<a href=\"");
htmlString.append(SERVLET_URL);
htmlString.append("/");
htmlString.append(accountId);
htmlString.append("\">");
htmlString.append(accountId);
htmlString.append("</a>");
htmlString.append("</li>");
});
htmlString.append("</ul>");
}
htmlString.append("</body>");
htmlString.append("</html>");
resp.setStatus(HttpStatus.OK_200);
resp.getWriter().write(htmlString.toString());
}
}

View File

@ -32,6 +32,7 @@ public class GroheOndusBindingConstants {
public static final String CHANNEL_TEMPERATURE_GUARD = "temperature_guard";
public static final String CHANNEL_VALVE_OPEN = "valve_open";
public static final String CHANNEL_WATERCONSUMPTION = "waterconsumption";
public static final String CHANNEL_WATERCONSUMPTION_SINCE_MIDNIGHT = "waterconsumption_since_midnight";
public static final String CHANNEL_TEMPERATURE = "temperature";
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_BATTERY = "battery";

View File

@ -12,7 +12,8 @@
*/
package org.openhab.binding.groheondus.internal.discovery;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.*;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.THING_TYPE_SENSE;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.THING_TYPE_SENSEGUARD;
import java.io.IOException;
import java.util.ArrayList;
@ -24,8 +25,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.grohe.ondus.api.OndusService;
import org.grohe.ondus.api.model.BaseAppliance;
import org.openhab.binding.groheondus.internal.handler.GroheOndusAccountHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
@ -34,6 +33,9 @@ import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.floriansw.ondus.api.OndusService;
import io.github.floriansw.ondus.api.model.BaseAppliance;
/**
* @author Florian Schmidt - Initial contribution
*/
@ -59,6 +61,9 @@ public class GroheOndusDiscoveryService extends AbstractDiscoveryService {
@Override
protected void startScan() {
// Remove old results - or they will stay there forever
removeOlderResults(getTimestampOfLastScan(), null, bridgeHandler.getThing().getUID());
OndusService service;
try {
service = bridgeHandler.getService();
@ -71,17 +76,16 @@ public class GroheOndusDiscoveryService extends AbstractDiscoveryService {
discoveredAppliances = service.appliances();
} catch (IOException e) {
logger.debug("Could not discover appliances.", e);
return;
}
discoveredAppliances.forEach(appliance -> {
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
ThingUID thingUID = null;
switch (appliance.getType()) {
case org.grohe.ondus.api.model.guard.Appliance.TYPE:
case io.github.floriansw.ondus.api.model.guard.Appliance.TYPE:
thingUID = new ThingUID(THING_TYPE_SENSEGUARD, bridgeUID, appliance.getApplianceId());
break;
case org.grohe.ondus.api.model.sense.Appliance.TYPE:
case io.github.floriansw.ondus.api.model.sense.Appliance.TYPE:
thingUID = new ThingUID(THING_TYPE_SENSE, bridgeUID, appliance.getApplianceId());
break;
default:

View File

@ -15,6 +15,7 @@ package org.openhab.binding.groheondus.internal.handler;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@ -22,8 +23,6 @@ import javax.security.auth.login.LoginException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.grohe.ondus.api.OndusService;
import org.openhab.binding.groheondus.internal.AccountServlet;
import org.openhab.binding.groheondus.internal.GroheOndusAccountConfiguration;
import org.openhab.core.storage.Storage;
import org.openhab.core.thing.Bridge;
@ -32,10 +31,11 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.osgi.service.http.HttpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.floriansw.ondus.api.OndusService;
/**
* @author Florian Schmidt and Arne Wohlert - Initial contribution
*/
@ -46,15 +46,12 @@ public class GroheOndusAccountHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(GroheOndusAccountHandler.class);
private HttpService httpService;
private Storage<String> storage;
private @Nullable AccountServlet accountServlet;
private @Nullable OndusService ondusService;
private @Nullable ScheduledFuture<?> refreshTokenFuture;
private @Nullable ScheduledFuture<?> reloginFuture;
public GroheOndusAccountHandler(Bridge bridge, HttpService httpService, Storage<String> storage) {
public GroheOndusAccountHandler(Bridge bridge, Storage<String> storage) {
super(bridge);
this.httpService = httpService;
this.storage = storage;
}
@ -66,43 +63,6 @@ public class GroheOndusAccountHandler extends BaseBridgeHandler {
return ret;
}
public void deleteRefreshToken() {
this.storage.remove(STORAGE_KEY_REFRESH_TOKEN);
this.initialize();
if (refreshTokenFuture != null) {
refreshTokenFuture.cancel(true);
}
}
public void setRefreshToken(String refreshToken) {
this.storage.put(STORAGE_KEY_REFRESH_TOKEN, refreshToken);
this.initialize();
}
private void scheduleTokenRefresh() {
if (ondusService != null) {
Instant expiresAt = ondusService.authorizationExpiresAt();
Duration between = Duration.between(Instant.now(), expiresAt);
refreshTokenFuture = scheduler.schedule(() -> {
OndusService ondusService = this.ondusService;
if (ondusService == null) {
logger.warn("Trying to refresh Ondus account without a service being present.");
return;
}
try {
setRefreshToken(ondusService.refreshAuthorization());
} catch (Exception e) {
logger.warn("Could not refresh authorization for GROHE ONDUS account", e);
}
}, between.getSeconds(), TimeUnit.SECONDS);
}
}
public boolean hasRefreshToken() {
return this.storage.containsKey(STORAGE_KEY_REFRESH_TOKEN);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Nothing to do for bridge
@ -110,56 +70,82 @@ public class GroheOndusAccountHandler extends BaseBridgeHandler {
@Override
public void dispose() {
super.dispose();
if (ondusService != null) {
ondusService = null;
}
if (accountServlet != null) {
accountServlet.dispose();
if (reloginFuture != null) {
reloginFuture.cancel(true);
}
if (refreshTokenFuture != null) {
refreshTokenFuture.cancel(true);
super.dispose();
}
private void login() {
GroheOndusAccountConfiguration config = getConfigAs(GroheOndusAccountConfiguration.class);
if (config.username == null || config.password == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.login.missing.credentials");
} else {
// Config appears to be ok, lets try
try {
OndusService ondusService;
if (storage.containsKey(STORAGE_KEY_REFRESH_TOKEN)) {
try {
logger.debug("Trying to login using refresh token");
ondusService = OndusService.login(storage.get(STORAGE_KEY_REFRESH_TOKEN));
} catch (LoginException e) {
logger.debug("Refresh token invalid, try again with username and password");
ondusService = OndusService.loginWebform(config.username, config.password);
}
} else {
logger.debug("No refresh token found, trying to log in using username and password");
ondusService = OndusService.loginWebform(config.username, config.password);
}
this.ondusService = ondusService;
// Assuming everything went fine...
Instant expiresAt = ondusService.authorizationExpiresAt();
// Refresh 5 minutes before expiry
Instant refreshTime = expiresAt.minus(5, ChronoUnit.MINUTES);
final OndusService ondusServiceInner = ondusService;
if (refreshTime.isAfter(Instant.now())) {
Duration durationUntilRefresh = Duration.between(Instant.now(), refreshTime);
reloginFuture = scheduler.schedule(() -> {
try {
logger.debug("Refreshing token");
this.storage.put(STORAGE_KEY_REFRESH_TOKEN, ondusServiceInner.refreshAuthorization());
logger.debug("Refreshed token, token expires at {}",
ondusServiceInner.authorizationExpiresAt());
} catch (Exception e) {
logger.debug("Could not refresh token for GROHE ONDUS account, removing refresh token", e);
this.storage.remove(STORAGE_KEY_REFRESH_TOKEN);
}
login();
}, durationUntilRefresh.getSeconds(), TimeUnit.SECONDS);
logger.debug("Scheduled token refresh at {}", refreshTime);
updateStatus(ThingStatus.ONLINE);
} else {
// Refresh time in the past (happens)
logger.debug("Refresh time for token was in the past, waiting a minute and retrying");
this.storage.remove(STORAGE_KEY_REFRESH_TOKEN);
reloginFuture = scheduler.schedule(this::login, 1, TimeUnit.MINUTES);
}
} catch (LoginException e) {
logger.debug("Grohe api login failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.login.failed");
} catch (IOException e) {
logger.debug("Communication error while logging into the grohe api", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
// Cleanup and retry
this.storage.remove(STORAGE_KEY_REFRESH_TOKEN);
reloginFuture = scheduler.schedule(this::login, 1, TimeUnit.MINUTES);
}
}
}
@Override
public void initialize() {
GroheOndusAccountConfiguration config = getConfigAs(GroheOndusAccountConfiguration.class);
if (this.accountServlet == null) {
this.accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this);
}
if ((config.username == null || config.password == null) && !this.hasRefreshToken()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"Need username/password or refreshToken");
return;
}
updateStatus(ThingStatus.UNKNOWN);
try {
if (storage.containsKey(STORAGE_KEY_REFRESH_TOKEN)) {
ondusService = OndusService.login(storage.get(STORAGE_KEY_REFRESH_TOKEN));
scheduleTokenRefresh();
} else {
// TODO: That's probably really inefficient, internally the loginWebform method acquires a refresh
// token, maybe there should be a way to obtain this token here, somehow.
ondusService = OndusService.loginWebform(config.username, config.password);
}
updateStatus(ThingStatus.ONLINE);
scheduler.submit(() -> getThing().getThings().forEach(thing -> {
GroheOndusBaseHandler thingHandler = (GroheOndusBaseHandler) thing.getHandler();
if (thingHandler != null) {
thingHandler.updateChannels();
}
}));
} catch (LoginException e) {
logger.debug("Grohe api login failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Login failed");
} catch (IOException e) {
logger.debug("Communication error while logging into the grohe api", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
login();
}
}

View File

@ -13,14 +13,11 @@
package org.openhab.binding.groheondus.internal.handler;
import java.io.IOException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.grohe.ondus.api.OndusService;
import org.grohe.ondus.api.model.BaseAppliance;
import org.grohe.ondus.api.model.Location;
import org.grohe.ondus.api.model.Room;
import org.openhab.binding.groheondus.internal.GroheOndusApplianceConfiguration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@ -29,9 +26,15 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.floriansw.ondus.api.OndusService;
import io.github.floriansw.ondus.api.model.BaseAppliance;
import io.github.floriansw.ondus.api.model.Location;
import io.github.floriansw.ondus.api.model.Room;
/**
* @author Florian Schmidt - Initial contribution
*/
@ -41,73 +44,83 @@ public abstract class GroheOndusBaseHandler<T extends BaseAppliance, M> extends
protected @Nullable GroheOndusApplianceConfiguration config;
private @Nullable ScheduledFuture<?> poller;
private final int applianceType;
public GroheOndusBaseHandler(Thing thing, int applianceType) {
// Used to space scheduled updates apart by 1 second to avoid rate limiting from service
private int thingCounter = 0;
public GroheOndusBaseHandler(Thing thing, int applianceType, int thingCounter) {
super(thing);
this.applianceType = applianceType;
this.thingCounter = thingCounter;
}
protected void schedulePolling() {
OndusService ondusService = getOndusService();
if (ondusService == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "@text/error.noservice");
return;
}
@Nullable
T appliance = getAppliance(ondusService);
if (appliance == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.empty.response");
return;
}
int pollingInterval = getPollingInterval(appliance);
ScheduledFuture<?> poller = this.poller;
if (poller != null) {
// Cancel any previous polling
poller.cancel(true);
}
this.poller = scheduler.scheduleWithFixedDelay(this::updateChannels, thingCounter, pollingInterval,
TimeUnit.SECONDS);
logger.debug("Scheduled polling every {}s for appliance {}", pollingInterval, thing.getUID());
}
@Override
public void dispose() {
logger.debug("Disposing scheduled updater for thing {}", thing.getUID());
ScheduledFuture<?> poller = this.poller;
if (poller != null) {
poller.cancel(true);
}
super.dispose();
}
@Override
public void initialize() {
config = getConfigAs(GroheOndusApplianceConfiguration.class);
OndusService ondusService = getOndusService();
if (ondusService == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"No initialized OndusService available from bridge.");
return;
}
@Nullable
T appliance = getAppliance(ondusService);
if (appliance == null) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.COMMUNICATION_ERROR, "Could not load appliance");
return;
}
int pollingInterval = getPollingInterval(appliance);
scheduler.scheduleWithFixedDelay(this::updateChannels, 0, pollingInterval, TimeUnit.SECONDS);
updateStatus(ThingStatus.UNKNOWN);
}
@Override
public void channelLinked(ChannelUID channelUID) {
super.channelLinked(channelUID);
OndusService ondusService = getOndusService();
if (ondusService == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"No initialized OndusService available from bridge.");
return;
}
@Nullable
T appliance = getAppliance(ondusService);
if (appliance == null) {
return;
}
updateChannel(channelUID, appliance, getLastDataPoint(appliance));
schedulePolling();
}
public void updateChannels() {
logger.debug("Updating channels for appliance {}", thing.getUID());
OndusService ondusService = getOndusService();
if (ondusService == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"No initialized OndusService available from bridge.");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "@text/error.noservice");
// Update channels to UNDEF
return;
}
@Nullable
T appliance = getAppliance(ondusService);
if (appliance == null) {
logger.debug("Updating channels failed since appliance is null, thing {}", thing.getUID());
return;
}
M measurement = getLastDataPoint(appliance);
getThing().getChannels().forEach(channel -> updateChannel(channel.getUID(), appliance, measurement));
updateStatus(ThingStatus.ONLINE);
if (measurement != null) {
getThing().getChannels().forEach(channel -> updateChannel(channel.getUID(), appliance, measurement));
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.failedtoloaddata");
}
}
protected abstract M getLastDataPoint(T appliance);
@ -142,14 +155,22 @@ public abstract class GroheOndusBaseHandler<T extends BaseAppliance, M> extends
protected @Nullable T getAppliance(OndusService ondusService) {
try {
BaseAppliance appliance = ondusService.getAppliance(getRoom(), config.applianceId).orElse(null);
if (appliance.getType() != getType()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Thing is not a GROHE SENSE Guard device.");
return null;
if (appliance != null) {
if (appliance.getType() != getType()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.wrongtype");
return null;
}
return (T) appliance;
} else {
logger.debug("getAppliance for thing {} returned null", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.failedtoloaddata");
getThing().getChannels().forEach(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
}
return (T) appliance;
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
getThing().getChannels().forEach(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
logger.debug("Could not load appliance", e);
}
return null;

View File

@ -12,7 +12,9 @@
*/
package org.openhab.binding.groheondus.internal.handler;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.*;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.THING_TYPE_BRIDGE_ACCOUNT;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.THING_TYPE_SENSE;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.THING_TYPE_SENSEGUARD;
import java.util.Arrays;
import java.util.Collection;
@ -22,7 +24,6 @@ import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.groheondus.internal.AccountsServlet;
import org.openhab.binding.groheondus.internal.discovery.GroheOndusDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.storage.StorageService;
@ -39,7 +40,6 @@ import org.osgi.framework.wiring.BundleWiring;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
/**
* @author Florian Schmidt and Arne Wohlert - Initial contribution
@ -50,16 +50,12 @@ public class GroheOndusHandlerFactory extends BaseThingHandlerFactory {
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private HttpService httpService;
private StorageService storageService;
private AccountsServlet accountsServlet;
private int thingCounter = 0;
@Activate
public GroheOndusHandlerFactory(@Reference HttpService httpService, @Reference StorageService storageService,
@Reference AccountsServlet accountsServlet) {
this.httpService = httpService;
public GroheOndusHandlerFactory(@Reference StorageService storageService) {
this.storageService = storageService;
this.accountsServlet = accountsServlet;
}
private static final Collection<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Arrays.asList(THING_TYPE_SENSEGUARD,
@ -75,15 +71,15 @@ public class GroheOndusHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_BRIDGE_ACCOUNT.equals(thingTypeUID)) {
GroheOndusAccountHandler handler = new GroheOndusAccountHandler((Bridge) thing, httpService,
GroheOndusAccountHandler handler = new GroheOndusAccountHandler((Bridge) thing,
storageService.getStorage(thing.getUID().toString(),
FrameworkUtil.getBundle(getClass()).adapt(BundleWiring.class).getClassLoader()));
onAccountCreated(thing, handler);
return handler;
} else if (THING_TYPE_SENSEGUARD.equals(thingTypeUID)) {
return new GroheOndusSenseGuardHandler(thing);
return new GroheOndusSenseGuardHandler(thing, thingCounter++);
} else if (THING_TYPE_SENSE.equals(thingTypeUID)) {
return new GroheOndusSenseHandler(thing);
return new GroheOndusSenseHandler(thing, thingCounter++);
}
return null;
@ -91,9 +87,6 @@ public class GroheOndusHandlerFactory extends BaseThingHandlerFactory {
private void onAccountCreated(Thing thing, GroheOndusAccountHandler handler) {
registerDeviceDiscoveryService(handler);
if (this.accountsServlet != null) {
this.accountsServlet.addAccount(thing);
}
}
@Override

View File

@ -12,26 +12,29 @@
*/
package org.openhab.binding.groheondus.internal.handler;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.*;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_CONFIG_TIMEFRAME;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_NAME;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_PRESSURE;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_TEMPERATURE_GUARD;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_VALVE_OPEN;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_WATERCONSUMPTION;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_WATERCONSUMPTION_SINCE_MIDNIGHT;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import javax.measure.quantity.Volume;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.grohe.ondus.api.OndusService;
import org.grohe.ondus.api.model.BaseApplianceCommand;
import org.grohe.ondus.api.model.BaseApplianceData;
import org.grohe.ondus.api.model.guard.Appliance;
import org.grohe.ondus.api.model.guard.ApplianceCommand;
import org.grohe.ondus.api.model.guard.ApplianceData;
import org.grohe.ondus.api.model.guard.ApplianceData.Data;
import org.grohe.ondus.api.model.guard.ApplianceData.Measurement;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
@ -43,10 +46,21 @@ import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.floriansw.ondus.api.OndusService;
import io.github.floriansw.ondus.api.model.BaseApplianceCommand;
import io.github.floriansw.ondus.api.model.BaseApplianceData;
import io.github.floriansw.ondus.api.model.guard.Appliance;
import io.github.floriansw.ondus.api.model.guard.ApplianceCommand;
import io.github.floriansw.ondus.api.model.guard.ApplianceData;
import io.github.floriansw.ondus.api.model.guard.ApplianceData.Data;
import io.github.floriansw.ondus.api.model.guard.ApplianceData.Measurement;
/**
* @author Florian Schmidt and Arne Wohlert - Initial contribution
*/
@ -58,8 +72,8 @@ public class GroheOndusSenseGuardHandler<T, M> extends GroheOndusBaseHandler<App
private final Logger logger = LoggerFactory.getLogger(GroheOndusSenseGuardHandler.class);
public GroheOndusSenseGuardHandler(Thing thing) {
super(thing, Appliance.TYPE);
public GroheOndusSenseGuardHandler(Thing thing, int thingCounter) {
super(thing, Appliance.TYPE, thingCounter);
}
@Override
@ -73,35 +87,52 @@ public class GroheOndusSenseGuardHandler<T, M> extends GroheOndusBaseHandler<App
@Override
protected void updateChannel(ChannelUID channelUID, Appliance appliance, Data dataPoint) {
String channelId = channelUID.getIdWithoutGroup();
State newState;
State newState = UnDefType.UNDEF;
Measurement lastMeasurement = getLastMeasurement(dataPoint);
switch (channelId) {
case CHANNEL_NAME:
newState = new StringType(appliance.getName());
break;
case CHANNEL_PRESSURE:
newState = new QuantityType<>(getLastMeasurement(dataPoint).getPressure(), Units.BAR);
newState = new QuantityType<>(lastMeasurement.getPressure(), Units.BAR);
break;
case CHANNEL_TEMPERATURE_GUARD:
newState = new QuantityType<>(getLastMeasurement(dataPoint).getTemperatureGuard(), SIUnits.CELSIUS);
newState = new QuantityType<>(lastMeasurement.getTemperatureGuard(), SIUnits.CELSIUS);
break;
case CHANNEL_VALVE_OPEN:
newState = getValveOpenType(appliance);
OnOffType valveOpenType = getValveOpenType(appliance);
if (valveOpenType != null) {
newState = valveOpenType;
}
break;
case CHANNEL_WATERCONSUMPTION:
newState = sumWaterCosumption(dataPoint);
newState = sumWaterConsumption(dataPoint);
break;
case CHANNEL_WATERCONSUMPTION_SINCE_MIDNIGHT:
newState = sumWaterConsumptionSinceMidnight(dataPoint);
break;
default:
throw new IllegalArgumentException("Channel " + channelUID + " not supported.");
}
if (newState != null) {
updateState(channelUID, newState);
}
updateState(channelUID, newState);
}
private DecimalType sumWaterCosumption(Data dataPoint) {
private QuantityType<Volume> sumWaterConsumptionSinceMidnight(Data dataPoint) {
ZonedDateTime earliestWithdrawal = ZonedDateTime.now(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS);
ZonedDateTime latestWithdrawal = earliestWithdrawal.plus(1, ChronoUnit.DAYS);
Double waterConsumption = dataPoint.getWithdrawals().stream()
.filter(e -> earliestWithdrawal.isBefore(e.starttime.toInstant().atZone(ZoneId.systemDefault()))
&& latestWithdrawal.isAfter(e.starttime.toInstant().atZone(ZoneId.systemDefault())))
.mapToDouble(withdrawal -> withdrawal.getWaterconsumption()).sum();
return new QuantityType<>(waterConsumption, Units.LITRE);
}
private QuantityType<Volume> sumWaterConsumption(Data dataPoint) {
Double waterConsumption = dataPoint.getWithdrawals().stream()
.mapToDouble(withdrawal -> withdrawal.getWaterconsumption()).sum();
return new DecimalType(waterConsumption);
return new QuantityType<Volume>(waterConsumption, Units.LITRE);
}
private Measurement getLastMeasurement(Data dataPoint) {
@ -126,8 +157,7 @@ public class GroheOndusSenseGuardHandler<T, M> extends GroheOndusBaseHandler<App
return null;
}
if (commandOptional.get().getType() != Appliance.TYPE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Thing is not a GROHE SENSE Guard device.");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.notsenseguard");
return null;
}
return ((ApplianceCommand) commandOptional.get()).getCommand().getValveOpen() ? OnOffType.ON : OnOffType.OFF;
@ -136,37 +166,46 @@ public class GroheOndusSenseGuardHandler<T, M> extends GroheOndusBaseHandler<App
@Override
protected Data getLastDataPoint(Appliance appliance) {
if (getOndusService() == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"No initialized OndusService available from bridge.");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "@text/error.noservice");
return new Data();
}
ApplianceData applianceData = getApplianceData(appliance);
if (applianceData == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Could not load data from API.");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.empty.response");
return new Data();
}
return applianceData.getData();
Data data = applianceData.getData();
Collections.sort(data.measurement, Comparator.comparing(e -> ZonedDateTime.parse(e.timestamp)));
Collections.sort(data.withdrawals, Comparator.comparing(e -> e.starttime));
return data;
}
private @Nullable ApplianceData getApplianceData(Appliance appliance) {
Instant from = fromTime();
Instant to = Instant.now();
// Truncated to date only inside api package
Instant to = Instant.now().plus(1, ChronoUnit.DAYS);
OndusService service = getOndusService();
if (service == null) {
return null;
}
try {
logger.debug("Fetching data for {} from {} to {}", thing.getUID(), from, to);
BaseApplianceData applianceData = service.applianceData(appliance, from, to).orElse(null);
if (applianceData.getType() != Appliance.TYPE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Thing is not a GROHE SENSE Guard device.");
return null;
if (applianceData != null) {
if (applianceData.getType() != Appliance.TYPE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.notsenseguard");
return null;
}
return (ApplianceData) applianceData;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.failedtoloaddata");
}
return (ApplianceData) applianceData;
} catch (IOException e) {
logger.debug("Could not load appliance data", e);
logger.debug("Could not load appliance data for {}", thing.getUID(), e);
}
return null;
}
@ -196,6 +235,11 @@ public class GroheOndusSenseGuardHandler<T, M> extends GroheOndusBaseHandler<App
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
updateChannels();
return;
}
if (!CHANNEL_VALVE_OPEN.equals(channelUID.getIdWithoutGroup())) {
return;
}

View File

@ -12,22 +12,22 @@
*/
package org.openhab.binding.groheondus.internal.handler;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.*;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_BATTERY;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_HUMIDITY;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_NAME;
import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_TEMPERATURE;
import java.io.IOException;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.grohe.ondus.api.OndusService;
import org.grohe.ondus.api.model.ApplianceStatus;
import org.grohe.ondus.api.model.BaseApplianceData;
import org.grohe.ondus.api.model.sense.Appliance;
import org.grohe.ondus.api.model.sense.ApplianceData;
import org.grohe.ondus.api.model.sense.ApplianceData.Measurement;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
@ -38,10 +38,19 @@ import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.floriansw.ondus.api.OndusService;
import io.github.floriansw.ondus.api.model.ApplianceStatus;
import io.github.floriansw.ondus.api.model.BaseApplianceData;
import io.github.floriansw.ondus.api.model.sense.Appliance;
import io.github.floriansw.ondus.api.model.sense.ApplianceData;
import io.github.floriansw.ondus.api.model.sense.ApplianceData.Measurement;
/**
* @author Florian Schmidt - Initial contribution
*/
@ -52,8 +61,8 @@ public class GroheOndusSenseHandler<T, M> extends GroheOndusBaseHandler<Applianc
private final Logger logger = LoggerFactory.getLogger(GroheOndusSenseHandler.class);
public GroheOndusSenseHandler(Thing thing) {
super(thing, Appliance.TYPE);
public GroheOndusSenseHandler(Thing thing, int thingCounter) {
super(thing, Appliance.TYPE, thingCounter);
}
@Override
@ -67,92 +76,104 @@ public class GroheOndusSenseHandler<T, M> extends GroheOndusBaseHandler<Applianc
@Override
protected void updateChannel(ChannelUID channelUID, Appliance appliance, Measurement measurement) {
String channelId = channelUID.getIdWithoutGroup();
State newState;
State newState = UnDefType.UNDEF;
switch (channelId) {
case CHANNEL_NAME:
newState = new StringType(appliance.getName());
break;
case CHANNEL_TEMPERATURE:
newState = new QuantityType<>(measurement.getTemperature(), SIUnits.CELSIUS);
if (measurement.getTemperature() != null) {
newState = new QuantityType<>(measurement.getTemperature(), SIUnits.CELSIUS);
}
break;
case CHANNEL_HUMIDITY:
newState = new QuantityType<>(measurement.getHumidity(), Units.PERCENT);
if (measurement.getHumidity() != null) {
newState = new QuantityType<>(measurement.getHumidity(), Units.PERCENT);
}
break;
case CHANNEL_BATTERY:
newState = new DecimalType(getBatteryStatus(appliance));
Integer batteryStatus = getBatteryStatus(appliance);
if (batteryStatus != null) {
newState = new DecimalType(batteryStatus);
}
break;
default:
throw new IllegalArgumentException("Channel " + channelUID + " not supported.");
}
if (newState != null) {
updateState(channelUID, newState);
}
updateState(channelUID, newState);
}
@Override
protected Measurement getLastDataPoint(Appliance appliance) {
if (getOndusService() == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"No initialized OndusService available from bridge.");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "@text/error.noservice");
return new Measurement();
}
ApplianceData applianceData = getApplianceData(appliance);
if (applianceData == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Could not load data from API.");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.empty.response");
return new Measurement();
}
List<Measurement> measurementList = applianceData.getData().getMeasurement();
Collections.sort(measurementList, Comparator.comparing(e -> ZonedDateTime.parse(e.timestamp)));
return measurementList.isEmpty() ? new Measurement() : measurementList.get(measurementList.size() - 1);
}
private int getBatteryStatus(Appliance appliance) {
private @Nullable Integer getBatteryStatus(Appliance appliance) {
OndusService ondusService = getOndusService();
if (ondusService == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"No initialized OndusService available from bridge.");
return -1;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "@text/error.noservice");
return null;
}
Optional<ApplianceStatus> applianceStatusOptional;
try {
applianceStatusOptional = ondusService.applianceStatus(appliance);
if (!applianceStatusOptional.isPresent()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Could not load data from API.");
return -1;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.empty.response");
return null;
}
return applianceStatusOptional.get().getBatteryStatus();
} catch (IOException e) {
logger.debug("Could not load appliance status", e);
}
return -1;
return null;
}
private @Nullable ApplianceData getApplianceData(Appliance appliance) {
Instant yesterday = Instant.now().minus(1, ChronoUnit.DAYS);
Instant today = Instant.now();
// Dates are stripped of time part inside library
Instant now = Instant.now();
Instant twoDaysAgo = now.minus(2, ChronoUnit.DAYS); // Devices only report once a day - at best
Instant tomorrow = now.plus(1, ChronoUnit.DAYS);
OndusService service = getOndusService();
if (service == null) {
return null;
}
try {
BaseApplianceData applianceData = service.applianceData(appliance, yesterday, today).orElse(null);
if (applianceData.getType() != Appliance.TYPE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Thing is not a GROHE SENSE device.");
return null;
logger.debug("Fetching data for {} from {} to {}", thing.getUID(), twoDaysAgo, tomorrow);
BaseApplianceData applianceData = service.applianceData(appliance, twoDaysAgo, tomorrow).orElse(null);
if (applianceData != null) {
if (applianceData.getType() != Appliance.TYPE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.notsense");
return null;
}
return (ApplianceData) applianceData;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.failedtoloaddata");
}
return (ApplianceData) applianceData;
} catch (IOException e) {
logger.debug("Could not load appliance data", e);
logger.debug("Could not load appliance data for {}", thing.getUID(), e);
}
return null;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
updateChannels();
}
}
}

View File

@ -1,58 +1,58 @@
# binding
binding.groheondus.name = GROHE ONDUS Binding
binding.groheondus.description = Provides an integration for GROHE Appliances in openHAB
binding.groheondus.name=GROHE ONDUS Binding
binding.groheondus.description=Provides an integration for GROHE Appliances in openHAB
# thing types
thing-type.groheondus.account.label = GROHE ONDUS Account
thing-type.groheondus.account.description = This is an interface to the GROHE ONDUS Account as it is used by the app. If username and password are not set, you can configure to use a `refreshToken` to login. Read the README to get more info.
thing-type.groheondus.sense.label = GROHE SENSE Appliance
thing-type.groheondus.sense.description = A SENSE device
thing-type.groheondus.senseguard.label = GROHE SENSE GUARD Appliance
thing-type.groheondus.senseguard.description = A SENSE GUARD device
thing-type.groheondus.account.label=GROHE ONDUS Account
thing-type.groheondus.account.description=This is an interface to the GROHE ONDUS Account as it is used by the app. If username and password are not set, you can configure to use a `refreshToken` to login. Read the README to get more info.
thing-type.groheondus.sense.label=GROHE SENSE Appliance
thing-type.groheondus.sense.description=A SENSE device
thing-type.groheondus.senseguard.label=GROHE SENSE GUARD Appliance
thing-type.groheondus.senseguard.description=A SENSE GUARD device
# thing types config
thing-type.config.groheondus.account.password.label = Password
thing-type.config.groheondus.account.password.description = Password as used in the GROHE ONDUS App.
thing-type.config.groheondus.account.username.label = Username
thing-type.config.groheondus.account.username.description = Username as used in the GROHE ONDUS App, usually your e-mail address.
thing-type.config.groheondus.sense.applianceId.label = Appliance ID
thing-type.config.groheondus.sense.applianceId.description = The UUID of the appliance as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.sense.locationId.label = Location ID
thing-type.config.groheondus.sense.locationId.description = The ID of the location the room is in as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.sense.pollingInterval.label = Polling Interval
thing-type.config.groheondus.sense.pollingInterval.description = The interval in seconds used to poll the API for new data.
thing-type.config.groheondus.sense.roomId.label = Room ID
thing-type.config.groheondus.sense.roomId.description = The ID of the room the appliance is in as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.senseguard.applianceId.label = Appliance ID
thing-type.config.groheondus.senseguard.applianceId.description = The UUID of the appliance as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.senseguard.locationId.label = Location ID
thing-type.config.groheondus.senseguard.locationId.description = The ID of the location the room is in as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.senseguard.pollingInterval.label = Polling Interval
thing-type.config.groheondus.senseguard.pollingInterval.description = The interval in seconds used to poll the API for new data. Defaults to the configuration of the appliance itself as retrieved from the API, usually 15 minutes.
thing-type.config.groheondus.senseguard.roomId.label = Room ID
thing-type.config.groheondus.senseguard.roomId.description = The ID of the room the appliance is in as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.account.password.label=Password
thing-type.config.groheondus.account.password.description=Password as used in the GROHE ONDUS App.
thing-type.config.groheondus.account.username.label=Username
thing-type.config.groheondus.account.username.description=Username as used in the GROHE ONDUS App, usually your e-mail address.
thing-type.config.groheondus.sense.applianceId.label=Appliance ID
thing-type.config.groheondus.sense.applianceId.description=The UUID of the appliance as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.sense.locationId.label=Location ID
thing-type.config.groheondus.sense.locationId.description=The ID of the location the room is in as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.sense.pollingInterval.label=Polling Interval
thing-type.config.groheondus.sense.pollingInterval.description=The interval in seconds used to poll the API for new data.
thing-type.config.groheondus.sense.roomId.label=Room ID
thing-type.config.groheondus.sense.roomId.description=The ID of the room the appliance is in as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.senseguard.applianceId.label=Appliance ID
thing-type.config.groheondus.senseguard.applianceId.description=The UUID of the appliance as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.senseguard.locationId.label=Location ID
thing-type.config.groheondus.senseguard.locationId.description=The ID of the location the room is in as retrieved from the GROHE ONDUS API.
thing-type.config.groheondus.senseguard.pollingInterval.label=Polling Interval
thing-type.config.groheondus.senseguard.pollingInterval.description=The interval in seconds used to poll the API for new data. Defaults to the configuration of the appliance itself as retrieved from the API, usually 15 minutes.
thing-type.config.groheondus.senseguard.roomId.label=Room ID
thing-type.config.groheondus.senseguard.roomId.description=The ID of the room the appliance is in as retrieved from the GROHE ONDUS API.
# channel types
channel-type.groheondus.humidity.label = Humidity
channel-type.groheondus.humidity.description = The humidity reported by the device
channel-type.groheondus.name.label = Appliance Name
channel-type.groheondus.name.description = The name of the appliance
channel-type.groheondus.pressure.label = Pressure
channel-type.groheondus.pressure.description = The pressure of your water supply
channel-type.groheondus.temperature.label = Temperature
channel-type.groheondus.temperature.description = The temperature reported by the device
channel-type.groheondus.temperature_guard.label = Temperature
channel-type.groheondus.temperature_guard.description = The ambient temperature of the appliance
channel-type.groheondus.valve_open.label = Valve Open
channel-type.groheondus.valve_open.description = Valve switch
channel-type.groheondus.waterconsumption.label = Water Consumption
channel-type.groheondus.waterconsumption.description = The amount of water consumed in the given time period.
channel-type.groheondus.humidity.label=Humidity
channel-type.groheondus.humidity.description=The humidity reported by the device
channel-type.groheondus.name.label=Appliance Name
channel-type.groheondus.name.description=The name of the appliance
channel-type.groheondus.pressure.label=Pressure
channel-type.groheondus.pressure.description=The pressure of your water supply
channel-type.groheondus.temperature.label=Temperature
channel-type.groheondus.temperature.description=The temperature reported by the device
channel-type.groheondus.temperature_guard.label=Temperature
channel-type.groheondus.temperature_guard.description=The ambient temperature of the appliance
channel-type.groheondus.valve_open.label=Valve Open
channel-type.groheondus.valve_open.description=Valve switch
channel-type.groheondus.waterconsumption.label=Water Consumption
channel-type.groheondus.waterconsumption.description=The amount of water consumed in the given time period.
# channel types config
channel-type.config.groheondus.waterconsumption.timeframe.label = Timeframe
channel-type.config.groheondus.waterconsumption.timeframe.description = The timeframe in days to get the water consumption of
channel-type.config.groheondus.waterconsumption.timeframe.label=Timeframe
channel-type.config.groheondus.waterconsumption.timeframe.description=The timeframe in days to get the water consumption of
# error messages
error.login.missing.credentials=Need username/password or refreshToken
error.login.failed=Login failed
error.empty.response=Could not load data from API.
error.failedtoloaddata=Failed to find appliance data
error.notsenseguard=Thing is not a GROHE SENSE Guard device.
error.notsense=Thing is not a GROHE SENSE device.
error.wrongtype=Unexpected type (Sense vs Sense Guard), check your configuration
error.noservice=No initialized OndusService available from bridge.

View File

@ -7,7 +7,8 @@
<bridge-type id="account">
<label>GROHE ONDUS Account</label>
<description>This is an interface to the GROHE ONDUS Account as it is used by the app. If username and password are
not set, you can configure to use a `refreshToken` to login. Read the README to get more info.</description>
not set, you can configure to use a `refreshToken` to login. Read the README to get more info.
</description>
<config-description>
<parameter name="username" type="text" required="false">
@ -35,6 +36,7 @@
<channel id="pressure" typeId="pressure"/>
<channel id="temperature_guard" typeId="temperature_guard"/>
<channel id="waterconsumption" typeId="waterconsumption"/>
<channel id="waterconsumption_since_midnight" typeId="waterconsumption_since_midnight"/>
<channel id="valve_open" typeId="valve_open"/>
</channels>
@ -53,10 +55,11 @@
<label>Location ID</label>
<description>The ID of the location the room is in as retrieved from the GROHE ONDUS API.</description>
</parameter>
<parameter name="pollingInterval" type="integer" required="false">
<parameter name="pollingInterval" type="integer" required="false" min="900">
<label>Polling Interval</label>
<description>The interval in seconds used to poll the API for new data. Defaults to the configuration of the
appliance itself as retrieved from the API, usually 15 minutes.</description>
appliance itself as retrieved from the API, usually 15 minutes.
</description>
</parameter>
</config-description>
</thing-type>
@ -91,7 +94,7 @@
<label>Location ID</label>
<description>The ID of the location the room is in as retrieved from the GROHE ONDUS API.</description>
</parameter>
<parameter name="pollingInterval" type="integer" required="false">
<parameter name="pollingInterval" type="integer" required="false" min="900">
<label>Polling Interval</label>
<description>The interval in seconds used to poll the API for new data.</description>
</parameter>
@ -132,10 +135,10 @@
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="waterconsumption">
<item-type>Number</item-type>
<item-type>Number:Volume</item-type>
<label>Water Consumption</label>
<description>The amount of water consumed in the given time period.</description>
<state readOnly="true"/>
<description>The amount of water consumed in the given time period in liters.</description>
<state readOnly="true" pattern="%.1f %unit%"/>
<config-description>
<parameter name="timeframe" type="integer" min="1" max="90" step="1" required="true">
<label>Timeframe</label>
@ -144,4 +147,10 @@
</parameter>
</config-description>
</channel-type>
<channel-type id="waterconsumption_since_midnight">
<item-type>Number:Volume</item-type>
<label>Water Consumption Since Midnight</label>
<description>The amount of water consumed since midnight in liters</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
</thing:thing-descriptions>