[netatmo] Switch to Code Granting process (#12726)
Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
parent
c12ed4bc8e
commit
9632a0a870
|
@ -11,17 +11,20 @@ See https://www.netatmo.com/ for details on their product.
|
||||||
|
|
||||||
## Binding Configuration
|
## Binding Configuration
|
||||||
|
|
||||||
Before setting up your 'Things', you will have to grant openHAB to access Netatmo API.
|
The binding requires you to register an Application with Netatmo Connect at [https://dev.netatmo.com/](https://dev.netatmo.com/) - this will get you a set of Client ID and Client Secret parameters to be used by your configuration.
|
||||||
Here is the procedure:
|
|
||||||
|
|
||||||
Create an application at https://dev.netatmo.com/dev/createapp
|
### Create Netatmo Application
|
||||||
|
|
||||||
The variables you will need to get to setup the binding are:
|
Follow instructions under:
|
||||||
|
|
||||||
|
1. Setting Up Your Account
|
||||||
|
1. Registering Your Application
|
||||||
|
1. Setting Redirect URI and webhook URI can be skipped, these will be provided by the binding.
|
||||||
|
|
||||||
|
Variables needed for the setup of the binding are:
|
||||||
|
|
||||||
* `<CLIENT_ID>` Your client ID taken from your App at https://dev.netatmo.com/apps
|
* `<CLIENT_ID>` Your client ID taken from your App at https://dev.netatmo.com/apps
|
||||||
* `<CLIENT_SECRET>` A token provided along with the `<CLIENT_ID>`.
|
* `<CLIENT_SECRET>` A token provided along with the `<CLIENT_ID>`.
|
||||||
* `<USERNAME>` The username you use to connect to the Netatmo API (usually your mail address).
|
|
||||||
* `<PASSWORD>` The password attached to the above username.
|
|
||||||
|
|
||||||
The binding has the following configuration options:
|
The binding has the following configuration options:
|
||||||
|
|
||||||
|
@ -31,18 +34,34 @@ The binding has the following configuration options:
|
||||||
| readFriends | Boolean | Enables or disables the discovery of guest weather stations. |
|
| readFriends | Boolean | Enables or disables the discovery of guest weather stations. |
|
||||||
|
|
||||||
|
|
||||||
## Bridge Configuration
|
## Netatmo Account (Bridge) Configuration
|
||||||
|
|
||||||
You will have to create at first a bridge to handle communication with your Netatmo Application.
|
You will have to create at first a bridge to handle communication with your Netatmo Application.
|
||||||
|
|
||||||
The Account bridge has the following configuration options:
|
The Account bridge has the following configuration elements:
|
||||||
|
|
||||||
- **clientId:** Client ID provided for the application you created on http://dev.netatmo.com/createapp.
|
| Parameter | Type | Required | Description |
|
||||||
- **clientSecret:** Client Secret provided for the application you created.
|
|-------------------|--------|----------|------------------------------------------------------------------------------------------------------------------------|
|
||||||
- **username:** Your Netatmo API username (email).
|
| clientId | String | Yes | Client ID provided for the application you created on http://dev.netatmo.com/createapp |
|
||||||
- **password:** Your Netatmo API password.
|
| clientSecret | String | Yes | Client Secret provided for the application you created |
|
||||||
- **webHookUrl:** Protocol, public IP and port to access openHAB server from Internet.
|
| webHookUrl | String | No | Protocol, public IP and port to access openHAB server from Internet |
|
||||||
- **reconnectInterval:** The reconnection interval to Netatmo API (in s).
|
| reconnectInterval | Number | No | The reconnection interval to Netatmo API (in s) |
|
||||||
|
| refreshToken | String | Yes* | The refresh token provided by Netatmo API after the granting process. Can be saved in case of file based configuration |
|
||||||
|
|
||||||
|
(*) Strictly said this parameter is not mandatory at first run, until you grant your binding on Netatmo Connect. Once present, you'll not have to grant again.
|
||||||
|
|
||||||
|
### Configure the Bridge
|
||||||
|
|
||||||
|
1. Complete the Netatmo Application Registration if you have not already done so, see above.
|
||||||
|
1. Make sure you have your _Client ID_ and _Client Secret_ identities available.
|
||||||
|
1. Add a new **"Netatmo Account"** thing. Choose new Id for the account, unless you like the generated one, put in the _Client ID_ and _Client Secret_ from the Netatmo Connect Application registration in their respective fields of the bridge configuration. Save the bridge.
|
||||||
|
1. The bridge thing will go _OFFLINE_ / _CONFIGURATION_ERROR_ - this is fine. You have to authorize this bridge with Netatmo Connect.
|
||||||
|
1. Go to the authorization page of your server. `http://<your openHAB address>:8080/netatmo/connect/<_CLIENT_ID_>`. Your newly added bridge should be listed there (no need for you to expose your openHAB server outside your local network for this).
|
||||||
|
1. Press the _"Authorize Thing"_ button. This will take you either to the login page of Netatmo Connect or directly to the authorization screen. Login and/or authorize the application. You will be returned and the entry should go green.
|
||||||
|
1. The binding will be updated with a refresh token and go _ONLINE_. The refresh token is used to re-authorize the bridge with Netatmo Connect Web API whenever required.
|
||||||
|
1. If you're using file based .things config file, copy the provided refresh token in the **refreshToken** parameter of your thing definition (example below).
|
||||||
|
|
||||||
|
Now that you have got your bridge _ONLINE_ you can now start a scan with the binding to auto discover your things.
|
||||||
|
|
||||||
|
|
||||||
## List of supported things
|
## List of supported things
|
||||||
|
@ -73,7 +92,7 @@ The Account bridge has the following configuration options:
|
||||||
### Webhook
|
### Webhook
|
||||||
|
|
||||||
Netatmo servers can send push notifications to the Netatmo Binding by using a callback URL.
|
Netatmo servers can send push notifications to the Netatmo Binding by using a callback URL.
|
||||||
The webhook URL is setup at binding level using "Webhook Address" parameter.
|
The webhook URL is setup at Netatmo Account level using "Webhook Address" parameter.
|
||||||
You will define here public way to access your openHAB server:
|
You will define here public way to access your openHAB server:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -83,7 +102,7 @@ http(s)://xx.yy.zz.ww:443
|
||||||
Your Netatmo App will be configured automatically by the bridge to the endpoint:
|
Your Netatmo App will be configured automatically by the bridge to the endpoint:
|
||||||
|
|
||||||
```
|
```
|
||||||
http(s)://xx.yy.zz.ww:443/netatmo
|
http(s)://xx.yy.zz.ww:443/netatmo/webhook/<_CLIENT_ID_>
|
||||||
```
|
```
|
||||||
|
|
||||||
Please be aware of Netatmo own limits regarding webhook usage that lead to a 24h ban-time when webhook does not answer 5 times.
|
Please be aware of Netatmo own limits regarding webhook usage that lead to a 24h ban-time when webhook does not answer 5 times.
|
||||||
|
@ -519,7 +538,7 @@ All these channels except at-home are read only.
|
||||||
## things/netatmo.things
|
## things/netatmo.things
|
||||||
|
|
||||||
```
|
```
|
||||||
Bridge netatmo:account:home "Netatmo Account" [clientId="", clientSecret="", username="", password=""] {
|
Bridge netatmo:account:home "Netatmo Account" [clientId="xxxxx", clientSecret="yyyy", refreshToken="zzzzz"] {
|
||||||
Bridge weather-station inside "Inside Weather Station" [id="70:ee:aa:aa:aa:aa"] {
|
Bridge weather-station inside "Inside Weather Station" [id="70:ee:aa:aa:aa:aa"] {
|
||||||
outdoor outside "Outside Module" [id="02:00:00:aa:aa:aa"] {
|
outdoor outside "Outside Module" [id="02:00:00:aa:aa:aa"] {
|
||||||
Channels:
|
Channels:
|
||||||
|
|
|
@ -27,9 +27,6 @@ public class NetatmoBindingConstants {
|
||||||
public static final String BINDING_ID = "netatmo";
|
public static final String BINDING_ID = "netatmo";
|
||||||
public static final String VENDOR = "Netatmo";
|
public static final String VENDOR = "Netatmo";
|
||||||
|
|
||||||
// Configuration keys
|
|
||||||
public static final String EQUIPMENT_ID = "id";
|
|
||||||
|
|
||||||
// Things properties
|
// Things properties
|
||||||
public static final String PROPERTY_CITY = "city";
|
public static final String PROPERTY_CITY = "city";
|
||||||
public static final String PROPERTY_COUNTRY = "country";
|
public static final String PROPERTY_COUNTRY = "country";
|
||||||
|
|
|
@ -68,11 +68,11 @@ import org.slf4j.LoggerFactory;
|
||||||
public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
|
public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
|
||||||
private final Logger logger = LoggerFactory.getLogger(NetatmoHandlerFactory.class);
|
private final Logger logger = LoggerFactory.getLogger(NetatmoHandlerFactory.class);
|
||||||
|
|
||||||
private final NetatmoDescriptionProvider stateDescriptionProvider;
|
|
||||||
private final HttpClient httpClient;
|
|
||||||
private final NADeserializer deserializer;
|
|
||||||
private final HttpService httpService;
|
|
||||||
private final BindingConfiguration configuration = new BindingConfiguration();
|
private final BindingConfiguration configuration = new BindingConfiguration();
|
||||||
|
private final NetatmoDescriptionProvider stateDescriptionProvider;
|
||||||
|
private final NADeserializer deserializer;
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
private final HttpService httpService;
|
||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
public NetatmoHandlerFactory(@Reference NetatmoDescriptionProvider stateDescriptionProvider,
|
public NetatmoHandlerFactory(@Reference NetatmoDescriptionProvider stateDescriptionProvider,
|
||||||
|
@ -80,8 +80,8 @@ public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
|
||||||
@Reference HttpService httpService, Map<String, @Nullable Object> config) {
|
@Reference HttpService httpService, Map<String, @Nullable Object> config) {
|
||||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||||
this.httpClient = factory.getCommonHttpClient();
|
this.httpClient = factory.getCommonHttpClient();
|
||||||
this.httpService = httpService;
|
|
||||||
this.deserializer = deserializer;
|
this.deserializer = deserializer;
|
||||||
|
this.httpService = httpService;
|
||||||
configChanged(config);
|
configChanged(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
|
||||||
|
|
||||||
private BaseThingHandler buildHandler(Thing thing, ModuleType moduleType) {
|
private BaseThingHandler buildHandler(Thing thing, ModuleType moduleType) {
|
||||||
if (ModuleType.ACCOUNT.equals(moduleType)) {
|
if (ModuleType.ACCOUNT.equals(moduleType)) {
|
||||||
return new ApiBridgeHandler((Bridge) thing, httpClient, httpService, deserializer, configuration);
|
return new ApiBridgeHandler((Bridge) thing, httpClient, deserializer, configuration, httpService);
|
||||||
}
|
}
|
||||||
CommonInterface handler = moduleType.isABridge() ? new DeviceHandler((Bridge) thing) : new ModuleHandler(thing);
|
CommonInterface handler = moduleType.isABridge() ? new DeviceHandler((Bridge) thing) : new ModuleHandler(thing);
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.netatmo.internal.api;
|
package org.openhab.binding.netatmo.internal.api;
|
||||||
|
|
||||||
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.PATH_OAUTH;
|
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
|
||||||
import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
|
import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -24,12 +24,14 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
|
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
|
||||||
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
|
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
|
||||||
import org.openhab.binding.netatmo.internal.api.dto.AccessTokenResponse;
|
import org.openhab.binding.netatmo.internal.api.dto.AccessTokenResponse;
|
||||||
import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration.Credentials;
|
import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration;
|
||||||
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -41,41 +43,57 @@ import org.slf4j.LoggerFactory;
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class AuthenticationApi extends RestManager {
|
public class AuthenticationApi extends RestManager {
|
||||||
private static final URI OAUTH_URI = getApiBaseBuilder().path(PATH_OAUTH).build();
|
private static final UriBuilder OAUTH_BUILDER = getApiBaseBuilder().path(PATH_OAUTH);
|
||||||
|
private static final UriBuilder AUTH_BUILDER = OAUTH_BUILDER.clone().path(SUB_PATH_AUTHORIZE);
|
||||||
|
private static final URI TOKEN_URI = OAUTH_BUILDER.clone().path(SUB_PATH_TOKEN).build();
|
||||||
|
|
||||||
private final ScheduledExecutorService scheduler;
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(AuthenticationApi.class);
|
private final Logger logger = LoggerFactory.getLogger(AuthenticationApi.class);
|
||||||
|
private final ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
private @Nullable ScheduledFuture<?> refreshTokenJob;
|
private Optional<ScheduledFuture<?>> refreshTokenJob = Optional.empty();
|
||||||
private Optional<AccessTokenResponse> tokenResponse = Optional.empty();
|
private Optional<AccessTokenResponse> tokenResponse = Optional.empty();
|
||||||
private String scope = "";
|
|
||||||
|
|
||||||
public AuthenticationApi(ApiBridgeHandler bridge, ScheduledExecutorService scheduler) {
|
public AuthenticationApi(ApiBridgeHandler bridge, ScheduledExecutorService scheduler) {
|
||||||
super(bridge, FeatureArea.NONE);
|
super(bridge, FeatureArea.NONE);
|
||||||
this.scheduler = scheduler;
|
this.scheduler = scheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void authenticate(Credentials credentials, Set<FeatureArea> features) throws NetatmoException {
|
public String authorize(ApiHandlerConfiguration credentials, Set<FeatureArea> features, @Nullable String code,
|
||||||
Set<FeatureArea> requestedFeatures = !features.isEmpty() ? features : FeatureArea.AS_SET;
|
@Nullable String redirectUri) throws NetatmoException {
|
||||||
scope = FeatureArea.toScopeString(requestedFeatures);
|
String clientId = credentials.clientId;
|
||||||
requestToken(credentials.clientId, credentials.clientSecret,
|
String clientSecret = credentials.clientSecret;
|
||||||
Map.of(USERNAME, credentials.username, PASSWORD, credentials.password, SCOPE, scope));
|
if (!(clientId.isBlank() || clientSecret.isBlank())) {
|
||||||
|
Map<String, String> params = new HashMap<>(Map.of(SCOPE, toScopeString(features)));
|
||||||
|
String refreshToken = credentials.refreshToken;
|
||||||
|
if (!refreshToken.isBlank()) {
|
||||||
|
params.put(REFRESH_TOKEN, refreshToken);
|
||||||
|
} else {
|
||||||
|
if (code != null && redirectUri != null) {
|
||||||
|
params.putAll(Map.of(REDIRECT_URI, redirectUri, CODE, code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (params.size() > 1) {
|
||||||
|
return requestToken(clientId, clientSecret, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Inconsistent configuration state, please file a bug report.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestToken(String id, String secret, Map<String, String> entries) throws NetatmoException {
|
private String requestToken(String id, String secret, Map<String, String> entries) throws NetatmoException {
|
||||||
Map<String, String> payload = new HashMap<>(entries);
|
Map<String, String> payload = new HashMap<>(entries);
|
||||||
payload.putAll(Map.of(GRANT_TYPE, entries.keySet().contains(PASSWORD) ? PASSWORD : REFRESH_TOKEN, CLIENT_ID, id,
|
payload.put(GRANT_TYPE, payload.keySet().contains(CODE) ? AUTHORIZATION_CODE : REFRESH_TOKEN);
|
||||||
CLIENT_SECRET, secret));
|
payload.putAll(Map.of(CLIENT_ID, id, CLIENT_SECRET, secret));
|
||||||
disconnect();
|
disconnect();
|
||||||
AccessTokenResponse response = post(OAUTH_URI, AccessTokenResponse.class, payload);
|
AccessTokenResponse response = post(TOKEN_URI, AccessTokenResponse.class, payload);
|
||||||
refreshTokenJob = scheduler.schedule(() -> {
|
refreshTokenJob = Optional.of(scheduler.schedule(() -> {
|
||||||
try {
|
try {
|
||||||
requestToken(id, secret, Map.of(REFRESH_TOKEN, response.getRefreshToken()));
|
requestToken(id, secret, Map.of(REFRESH_TOKEN, response.getRefreshToken()));
|
||||||
} catch (NetatmoException e) {
|
} catch (NetatmoException e) {
|
||||||
logger.warn("Unable to refresh access token : {}", e.getMessage());
|
logger.warn("Unable to refresh access token : {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}, Math.round(response.getExpiresIn() * 0.8), TimeUnit.SECONDS);
|
}, Math.round(response.getExpiresIn() * 0.8), TimeUnit.SECONDS));
|
||||||
tokenResponse = Optional.of(response);
|
tokenResponse = Optional.of(response);
|
||||||
|
return response.getRefreshToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disconnect() {
|
public void disconnect() {
|
||||||
|
@ -83,11 +101,8 @@ public class AuthenticationApi extends RestManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
ScheduledFuture<?> job = refreshTokenJob;
|
refreshTokenJob.ifPresent(job -> job.cancel(true));
|
||||||
if (job != null) {
|
refreshTokenJob = Optional.empty();
|
||||||
job.cancel(true);
|
|
||||||
}
|
|
||||||
refreshTokenJob = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable String getAuthorization() {
|
public @Nullable String getAuthorization() {
|
||||||
|
@ -95,12 +110,20 @@ public class AuthenticationApi extends RestManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matchesScopes(Set<Scope> requiredScopes) {
|
public boolean matchesScopes(Set<Scope> requiredScopes) {
|
||||||
// either we do not require any scope, either connected and all scopes available
|
return requiredScopes.isEmpty() // either we do not require any scope, either connected and all scopes available
|
||||||
return requiredScopes.isEmpty()
|
|
||||||
|| (isConnected() && tokenResponse.map(at -> at.getScope().containsAll(requiredScopes)).orElse(false));
|
|| (isConnected() && tokenResponse.map(at -> at.getScope().containsAll(requiredScopes)).orElse(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isConnected() {
|
public boolean isConnected() {
|
||||||
return !tokenResponse.isEmpty();
|
return tokenResponse.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toScopeString(Set<FeatureArea> features) {
|
||||||
|
return FeatureArea.toScopeString(features.isEmpty() ? FeatureArea.AS_SET : features);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UriBuilder getAuthorizationBuilder(String clientId, Set<FeatureArea> features) {
|
||||||
|
return AUTH_BUILDER.clone().queryParam(CLIENT_ID, clientId).queryParam(SCOPE, toScopeString(features))
|
||||||
|
.queryParam(STATE, clientId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ public class NetatmoException extends IOException {
|
||||||
public @Nullable String getMessage() {
|
public @Nullable String getMessage() {
|
||||||
String message = super.getMessage();
|
String message = super.getMessage();
|
||||||
return message == null ? null
|
return message == null ? null
|
||||||
: String.format("Rest call failed: statusCode=%s, message=%s", statusCode, message);
|
: ServiceError.UNKNOWN.equals(statusCode) ? message
|
||||||
|
: String.format("Rest call failed: statusCode=%s, message=%s", statusCode, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,9 +56,10 @@ public class SecurityApi extends RestManager {
|
||||||
* @param uri Your webhook callback url (required)
|
* @param uri Your webhook callback url (required)
|
||||||
* @throws NetatmoException If fail to call the API, e.g. server error or deserializing
|
* @throws NetatmoException If fail to call the API, e.g. server error or deserializing
|
||||||
*/
|
*/
|
||||||
public void addwebhook(URI uri) throws NetatmoException {
|
public boolean addwebhook(URI uri) throws NetatmoException {
|
||||||
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_ADDWEBHOOK, PARAM_URL, uri.toString());
|
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_ADDWEBHOOK, PARAM_URL, uri.toString());
|
||||||
post(uriBuilder, ApiResponse.Ok.class, null, null);
|
post(uriBuilder, ApiResponse.Ok.class, null, null);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<HomeEvent> getPersonEvents(String homeId, String personId) throws NetatmoException {
|
public Collection<HomeEvent> getPersonEvents(String homeId, String personId) throws NetatmoException {
|
||||||
|
|
|
@ -116,7 +116,9 @@ public class NetatmoConstants {
|
||||||
// Netatmo API urls
|
// Netatmo API urls
|
||||||
public static final String URL_API = "https://api.netatmo.com/";
|
public static final String URL_API = "https://api.netatmo.com/";
|
||||||
public static final String URL_APP = "https://app.netatmo.net/";
|
public static final String URL_APP = "https://app.netatmo.net/";
|
||||||
public static final String PATH_OAUTH = "oauth2/token";
|
public static final String PATH_OAUTH = "oauth2";
|
||||||
|
public static final String SUB_PATH_TOKEN = "token";
|
||||||
|
public static final String SUB_PATH_AUTHORIZE = "authorize";
|
||||||
public static final String PATH_API = "api";
|
public static final String PATH_API = "api";
|
||||||
public static final String PATH_COMMAND = "command";
|
public static final String PATH_COMMAND = "command";
|
||||||
public static final String PATH_STATE = "setstate";
|
public static final String PATH_STATE = "setstate";
|
||||||
|
@ -148,6 +150,9 @@ public class NetatmoConstants {
|
||||||
public static final String PARAM_FAVORITES = "get_favorites";
|
public static final String PARAM_FAVORITES = "get_favorites";
|
||||||
public static final String PARAM_STATUS = "status";
|
public static final String PARAM_STATUS = "status";
|
||||||
|
|
||||||
|
// Autentication process params
|
||||||
|
public static final String PARAM_ERROR = "error";
|
||||||
|
|
||||||
// Global variables
|
// Global variables
|
||||||
public static final int THERM_MAX_SETPOINT = 30;
|
public static final int THERM_MAX_SETPOINT = 30;
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,6 @@
|
||||||
package org.openhab.binding.netatmo.internal.config;
|
package org.openhab.binding.netatmo.internal.config;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
|
||||||
import org.openhab.binding.netatmo.internal.api.NetatmoException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link ApiHandlerConfiguration} is responsible for holding configuration
|
* The {@link ApiHandlerConfiguration} is responsible for holding configuration
|
||||||
|
@ -24,39 +22,23 @@ import org.openhab.binding.netatmo.internal.api.NetatmoException;
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class ApiHandlerConfiguration {
|
public class ApiHandlerConfiguration {
|
||||||
public class Credentials {
|
public static final String CLIENT_ID = "clientId";
|
||||||
public final String clientId, clientSecret, username, password;
|
public static final String REFRESH_TOKEN = "refreshToken";
|
||||||
|
|
||||||
private Credentials(@Nullable String clientId, @Nullable String clientSecret, @Nullable String username,
|
public String clientId = "";
|
||||||
@Nullable String password) throws NetatmoException {
|
public String clientSecret = "";
|
||||||
this.clientSecret = checkMandatory(clientSecret, "@text/conf-error-no-client-secret");
|
public String refreshToken = "";
|
||||||
this.username = checkMandatory(username, "@text/conf-error-no-username");
|
public String webHookUrl = "";
|
||||||
this.password = checkMandatory(password, "@text/conf-error-no-password");
|
|
||||||
this.clientId = checkMandatory(clientId, "@text/conf-error-no-client-id");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String checkMandatory(@Nullable String value, String error) throws NetatmoException {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
throw new NetatmoException(error);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Credentials [clientId=" + clientId + ", username=" + username
|
|
||||||
+ ", password=******, clientSecret=******]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private @Nullable String clientId;
|
|
||||||
private @Nullable String clientSecret;
|
|
||||||
private @Nullable String username;
|
|
||||||
private @Nullable String password;
|
|
||||||
public @Nullable String webHookUrl;
|
|
||||||
public int reconnectInterval = 300;
|
public int reconnectInterval = 300;
|
||||||
|
|
||||||
public Credentials getCredentials() throws NetatmoException {
|
public ConfigurationLevel check() {
|
||||||
return new Credentials(clientId, clientSecret, username, password);
|
if (clientId.isBlank()) {
|
||||||
|
return ConfigurationLevel.EMPTY_CLIENT_ID;
|
||||||
|
} else if (clientSecret.isBlank()) {
|
||||||
|
return ConfigurationLevel.EMPTY_CLIENT_SECRET;
|
||||||
|
} else if (refreshToken.isBlank()) {
|
||||||
|
return ConfigurationLevel.REFRESH_TOKEN_NEEDED;
|
||||||
|
}
|
||||||
|
return ConfigurationLevel.COMPLETED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* 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.netatmo.internal.config;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link ConfigurationLevel} describes configuration levels of a given account thing
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public enum ConfigurationLevel {
|
||||||
|
EMPTY_CLIENT_ID("@text/conf-error-no-client-id"),
|
||||||
|
EMPTY_CLIENT_SECRET("@text/conf-error-no-client-secret"),
|
||||||
|
REFRESH_TOKEN_NEEDED("@text/conf-error-grant-needed"),
|
||||||
|
COMPLETED("");
|
||||||
|
|
||||||
|
public String message;
|
||||||
|
|
||||||
|
ConfigurationLevel(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class NAThingConfiguration {
|
public class NAThingConfiguration {
|
||||||
|
public static final String ID = "id";
|
||||||
|
|
||||||
public String id = "";
|
public String id = "";
|
||||||
public int refreshInterval = -1;
|
public int refreshInterval = -1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,6 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.netatmo.internal.discovery;
|
package org.openhab.binding.netatmo.internal.discovery;
|
||||||
|
|
||||||
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.EQUIPMENT_ID;
|
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -28,7 +26,7 @@ import org.openhab.binding.netatmo.internal.api.data.ModuleType;
|
||||||
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
|
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
|
||||||
import org.openhab.binding.netatmo.internal.api.dto.NAMain;
|
import org.openhab.binding.netatmo.internal.api.dto.NAMain;
|
||||||
import org.openhab.binding.netatmo.internal.api.dto.NAModule;
|
import org.openhab.binding.netatmo.internal.api.dto.NAModule;
|
||||||
import org.openhab.binding.netatmo.internal.config.BindingConfiguration;
|
import org.openhab.binding.netatmo.internal.config.NAThingConfiguration;
|
||||||
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
||||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||||
|
@ -52,7 +50,7 @@ public class NetatmoDiscoveryService extends AbstractDiscoveryService implements
|
||||||
private static final int DISCOVER_TIMEOUT_SECONDS = 5;
|
private static final int DISCOVER_TIMEOUT_SECONDS = 5;
|
||||||
private final Logger logger = LoggerFactory.getLogger(NetatmoDiscoveryService.class);
|
private final Logger logger = LoggerFactory.getLogger(NetatmoDiscoveryService.class);
|
||||||
private @Nullable ApiBridgeHandler handler;
|
private @Nullable ApiBridgeHandler handler;
|
||||||
private @Nullable BindingConfiguration config;
|
private boolean readFriends;
|
||||||
|
|
||||||
public NetatmoDiscoveryService() {
|
public NetatmoDiscoveryService() {
|
||||||
super(ModuleType.AS_SET.stream().filter(mt -> !SKIPPED_TYPES.contains(mt)).map(mt -> mt.thingTypeUID)
|
super(ModuleType.AS_SET.stream().filter(mt -> !SKIPPED_TYPES.contains(mt)).map(mt -> mt.thingTypeUID)
|
||||||
|
@ -61,9 +59,8 @@ public class NetatmoDiscoveryService extends AbstractDiscoveryService implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startScan() {
|
public void startScan() {
|
||||||
BindingConfiguration localConf = config;
|
|
||||||
ApiBridgeHandler localHandler = handler;
|
ApiBridgeHandler localHandler = handler;
|
||||||
if (localHandler != null && localConf != null) {
|
if (localHandler != null) {
|
||||||
ThingUID apiBridgeUID = localHandler.getThing().getUID();
|
ThingUID apiBridgeUID = localHandler.getThing().getUID();
|
||||||
try {
|
try {
|
||||||
AircareApi airCareApi = localHandler.getRestManager(AircareApi.class);
|
AircareApi airCareApi = localHandler.getRestManager(AircareApi.class);
|
||||||
|
@ -73,7 +70,7 @@ public class NetatmoDiscoveryService extends AbstractDiscoveryService implements
|
||||||
body.getElements().stream().forEach(homeCoach -> createThing(homeCoach, apiBridgeUID));
|
body.getElements().stream().forEach(homeCoach -> createThing(homeCoach, apiBridgeUID));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (localConf.readFriends) {
|
if (readFriends) {
|
||||||
WeatherApi weatherApi = localHandler.getRestManager(WeatherApi.class);
|
WeatherApi weatherApi = localHandler.getRestManager(WeatherApi.class);
|
||||||
if (weatherApi != null) { // Search favorite stations
|
if (weatherApi != null) { // Search favorite stations
|
||||||
ListBodyResponse<NAMain> body = weatherApi.getStationsData(null, true).getBody();
|
ListBodyResponse<NAMain> body = weatherApi.getStationsData(null, true).getBody();
|
||||||
|
@ -127,7 +124,8 @@ public class NetatmoDiscoveryService extends AbstractDiscoveryService implements
|
||||||
private ThingUID createThing(NAModule module, @Nullable ThingUID bridgeUID) {
|
private ThingUID createThing(NAModule module, @Nullable ThingUID bridgeUID) {
|
||||||
ThingUID moduleUID = findThingUID(module.getType(), module.getId(), bridgeUID);
|
ThingUID moduleUID = findThingUID(module.getType(), module.getId(), bridgeUID);
|
||||||
DiscoveryResultBuilder resultBuilder = DiscoveryResultBuilder.create(moduleUID)
|
DiscoveryResultBuilder resultBuilder = DiscoveryResultBuilder.create(moduleUID)
|
||||||
.withProperty(EQUIPMENT_ID, module.getId()).withRepresentationProperty(EQUIPMENT_ID)
|
.withProperty(NAThingConfiguration.ID, module.getId())
|
||||||
|
.withRepresentationProperty(NAThingConfiguration.ID)
|
||||||
.withLabel(module.getName() != null ? module.getName() : module.getId());
|
.withLabel(module.getName() != null ? module.getName() : module.getId());
|
||||||
if (bridgeUID != null) {
|
if (bridgeUID != null) {
|
||||||
resultBuilder.withBridge(bridgeUID);
|
resultBuilder.withBridge(bridgeUID);
|
||||||
|
@ -140,7 +138,7 @@ public class NetatmoDiscoveryService extends AbstractDiscoveryService implements
|
||||||
public void setThingHandler(ThingHandler handler) {
|
public void setThingHandler(ThingHandler handler) {
|
||||||
if (handler instanceof ApiBridgeHandler) {
|
if (handler instanceof ApiBridgeHandler) {
|
||||||
this.handler = (ApiBridgeHandler) handler;
|
this.handler = (ApiBridgeHandler) handler;
|
||||||
this.config = ((ApiBridgeHandler) handler).getConfiguration();
|
this.readFriends = ((ApiBridgeHandler) handler).getReadFriends();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,8 @@ import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
|
@ -42,13 +44,16 @@ import org.openhab.binding.netatmo.internal.api.ApiError;
|
||||||
import org.openhab.binding.netatmo.internal.api.AuthenticationApi;
|
import org.openhab.binding.netatmo.internal.api.AuthenticationApi;
|
||||||
import org.openhab.binding.netatmo.internal.api.NetatmoException;
|
import org.openhab.binding.netatmo.internal.api.NetatmoException;
|
||||||
import org.openhab.binding.netatmo.internal.api.RestManager;
|
import org.openhab.binding.netatmo.internal.api.RestManager;
|
||||||
|
import org.openhab.binding.netatmo.internal.api.SecurityApi;
|
||||||
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
|
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
|
||||||
import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration;
|
import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration;
|
||||||
import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration.Credentials;
|
|
||||||
import org.openhab.binding.netatmo.internal.config.BindingConfiguration;
|
import org.openhab.binding.netatmo.internal.config.BindingConfiguration;
|
||||||
|
import org.openhab.binding.netatmo.internal.config.ConfigurationLevel;
|
||||||
import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
|
import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
|
||||||
import org.openhab.binding.netatmo.internal.discovery.NetatmoDiscoveryService;
|
import org.openhab.binding.netatmo.internal.discovery.NetatmoDiscoveryService;
|
||||||
import org.openhab.binding.netatmo.internal.webhook.NetatmoServlet;
|
import org.openhab.binding.netatmo.internal.servlet.GrantServlet;
|
||||||
|
import org.openhab.binding.netatmo.internal.servlet.WebhookServlet;
|
||||||
|
import org.openhab.core.config.core.Configuration;
|
||||||
import org.openhab.core.thing.Bridge;
|
import org.openhab.core.thing.Bridge;
|
||||||
import org.openhab.core.thing.ChannelUID;
|
import org.openhab.core.thing.ChannelUID;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
|
@ -73,64 +78,95 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
|
||||||
private final BindingConfiguration bindingConf;
|
private final BindingConfiguration bindingConf;
|
||||||
private final HttpService httpService;
|
|
||||||
private final AuthenticationApi connectApi;
|
private final AuthenticationApi connectApi;
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
private final NADeserializer deserializer;
|
private final NADeserializer deserializer;
|
||||||
|
private final HttpService httpService;
|
||||||
|
|
||||||
private Optional<ScheduledFuture<?>> connectJob = Optional.empty();
|
private Optional<ScheduledFuture<?>> connectJob = Optional.empty();
|
||||||
private Optional<NetatmoServlet> servlet = Optional.empty();
|
|
||||||
private @NonNullByDefault({}) ApiHandlerConfiguration thingConf;
|
|
||||||
|
|
||||||
private Map<Class<? extends RestManager>, RestManager> managers = new HashMap<>();
|
private Map<Class<? extends RestManager>, RestManager> managers = new HashMap<>();
|
||||||
|
private @Nullable WebhookServlet webHookServlet;
|
||||||
|
private @Nullable GrantServlet grantServlet;
|
||||||
|
|
||||||
public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, HttpService httpService, NADeserializer deserializer,
|
public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, NADeserializer deserializer,
|
||||||
BindingConfiguration configuration) {
|
BindingConfiguration configuration, HttpService httpService) {
|
||||||
super(bridge);
|
super(bridge);
|
||||||
this.bindingConf = configuration;
|
this.bindingConf = configuration;
|
||||||
this.httpService = httpService;
|
|
||||||
this.connectApi = new AuthenticationApi(this, scheduler);
|
this.connectApi = new AuthenticationApi(this, scheduler);
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
this.deserializer = deserializer;
|
this.deserializer = deserializer;
|
||||||
|
this.httpService = httpService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
logger.debug("Initializing Netatmo API bridge handler.");
|
logger.debug("Initializing Netatmo API bridge handler.");
|
||||||
thingConf = getConfigAs(ApiHandlerConfiguration.class);
|
|
||||||
updateStatus(ThingStatus.UNKNOWN);
|
updateStatus(ThingStatus.UNKNOWN);
|
||||||
scheduler.execute(() -> {
|
scheduler.execute(() -> openConnection(null, null));
|
||||||
openConnection();
|
|
||||||
String webHookUrl = thingConf.webHookUrl;
|
|
||||||
if (webHookUrl != null && !webHookUrl.isBlank()) {
|
|
||||||
servlet = Optional.of(new NetatmoServlet(httpService, this, webHookUrl));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openConnection() {
|
public void openConnection(@Nullable String code, @Nullable String redirectUri) {
|
||||||
try {
|
ApiHandlerConfiguration configuration = getConfiguration();
|
||||||
Credentials credentials = thingConf.getCredentials();
|
ConfigurationLevel level = configuration.check();
|
||||||
logger.debug("Connecting to Netatmo API.");
|
switch (level) {
|
||||||
try {
|
case EMPTY_CLIENT_ID:
|
||||||
connectApi.authenticate(credentials, bindingConf.features);
|
case EMPTY_CLIENT_SECRET:
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, level.message);
|
||||||
getThing().getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler).filter(Objects::nonNull)
|
break;
|
||||||
.map(CommonInterface.class::cast).forEach(CommonInterface::expireData);
|
case REFRESH_TOKEN_NEEDED:
|
||||||
} catch (NetatmoException e) {
|
if (code == null || redirectUri == null) {
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
GrantServlet servlet = new GrantServlet(this, httpService);
|
||||||
prepareReconnection();
|
servlet.startListening();
|
||||||
}
|
this.grantServlet = servlet;
|
||||||
} catch (NetatmoException e) {
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, level.message);
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
break;
|
||||||
|
} // else we can proceed to get the token refresh
|
||||||
|
case COMPLETED:
|
||||||
|
try {
|
||||||
|
logger.debug("Connecting to Netatmo API.");
|
||||||
|
|
||||||
|
String refreshToken = connectApi.authorize(configuration, bindingConf.features, code, redirectUri);
|
||||||
|
|
||||||
|
if (configuration.refreshToken.isBlank()) {
|
||||||
|
Configuration thingConfig = editConfiguration();
|
||||||
|
thingConfig.put(ApiHandlerConfiguration.REFRESH_TOKEN, refreshToken);
|
||||||
|
updateConfiguration(thingConfig);
|
||||||
|
configuration = getConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configuration.webHookUrl.isBlank()) {
|
||||||
|
SecurityApi securityApi = getRestManager(SecurityApi.class);
|
||||||
|
if (securityApi != null) {
|
||||||
|
WebhookServlet servlet = new WebhookServlet(this, httpService, deserializer, securityApi,
|
||||||
|
configuration.webHookUrl);
|
||||||
|
servlet.startListening();
|
||||||
|
this.webHookServlet = servlet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
|
||||||
|
getThing().getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler)
|
||||||
|
.filter(Objects::nonNull).map(CommonInterface.class::cast)
|
||||||
|
.forEach(CommonInterface::expireData);
|
||||||
|
|
||||||
|
} catch (NetatmoException e) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||||
|
prepareReconnection(code, redirectUri);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareReconnection() {
|
public ApiHandlerConfiguration getConfiguration() {
|
||||||
|
return getConfigAs(ApiHandlerConfiguration.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareReconnection(@Nullable String code, @Nullable String redirectUri) {
|
||||||
connectApi.disconnect();
|
connectApi.disconnect();
|
||||||
freeConnectJob();
|
freeConnectJob();
|
||||||
connectJob = Optional
|
connectJob = Optional.of(scheduler.schedule(() -> openConnection(code, redirectUri),
|
||||||
.of(scheduler.schedule(() -> openConnection(), thingConf.reconnectInterval, TimeUnit.SECONDS));
|
getConfiguration().reconnectInterval, TimeUnit.SECONDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void freeConnectJob() {
|
private void freeConnectJob() {
|
||||||
|
@ -141,8 +177,14 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
logger.debug("Shutting down Netatmo API bridge handler.");
|
logger.debug("Shutting down Netatmo API bridge handler.");
|
||||||
servlet.ifPresent(servlet -> servlet.dispose());
|
WebhookServlet localWebHook = this.webHookServlet;
|
||||||
servlet = Optional.empty();
|
if (localWebHook != null) {
|
||||||
|
localWebHook.dispose();
|
||||||
|
}
|
||||||
|
GrantServlet localGrant = this.grantServlet;
|
||||||
|
if (localGrant != null) {
|
||||||
|
localGrant.dispose();
|
||||||
|
}
|
||||||
connectApi.dispose();
|
connectApi.dispose();
|
||||||
freeConnectJob();
|
freeConnectJob();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
@ -153,11 +195,6 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
||||||
logger.debug("Netatmo Bridge is read-only and does not handle commands");
|
logger.debug("Netatmo Bridge is read-only and does not handle commands");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
|
||||||
return Set.of(NetatmoDiscoveryService.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <T extends RestManager> @Nullable T getRestManager(Class<T> clazz) {
|
public <T extends RestManager> @Nullable T getRestManager(Class<T> clazz) {
|
||||||
if (!managers.containsKey(clazz)) {
|
if (!managers.containsKey(clazz)) {
|
||||||
|
@ -218,24 +255,33 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
||||||
return executeUri(uri, method, clazz, payload, contentType, retryCount - 1);
|
return executeUri(uri, method, clazz, payload, contentType, retryCount - 1);
|
||||||
}
|
}
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/request-time-out");
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/request-time-out");
|
||||||
prepareReconnection();
|
prepareReconnection(null, null);
|
||||||
throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
|
throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BindingConfiguration getConfiguration() {
|
public boolean getReadFriends() {
|
||||||
return bindingConf;
|
return bindingConf.readFriends;
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<NetatmoServlet> getServlet() {
|
|
||||||
return servlet;
|
|
||||||
}
|
|
||||||
|
|
||||||
public NADeserializer getDeserializer() {
|
|
||||||
return deserializer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isConnected() {
|
public boolean isConnected() {
|
||||||
return connectApi.isConnected();
|
return connectApi.isConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return (String) getThing().getConfiguration().get(ApiHandlerConfiguration.CLIENT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UriBuilder formatAuthorizationUrl() {
|
||||||
|
return AuthenticationApi.getAuthorizationBuilder(getId(), bindingConf.features);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||||
|
return Set.of(NetatmoDiscoveryService.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<WebhookServlet> getWebHookServlet() {
|
||||||
|
return Optional.ofNullable(webHookServlet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,7 @@ public interface CommonInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
default String getId() {
|
default String getId() {
|
||||||
return (String) getThing().getConfiguration().get("id");
|
return (String) getThing().getConfiguration().get(NAThingConfiguration.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
default Stream<Channel> getActiveChannels() {
|
default Stream<Channel> getActiveChannels() {
|
||||||
|
|
|
@ -17,19 +17,18 @@ import java.util.Optional;
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
||||||
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
|
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
|
||||||
import org.openhab.binding.netatmo.internal.webhook.NetatmoServlet;
|
import org.openhab.binding.netatmo.internal.servlet.WebhookServlet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link EventCapability} is the base class for handlers
|
* {@link EventCapability} is the base class for handlers subject to receive event notifications.
|
||||||
* subject to receive event notifications. This class registers to webhookservlet so
|
* This class registers to NetatmoServletService so it can be notified when an event arrives.
|
||||||
* it can be notified when an event arrives.
|
|
||||||
*
|
*
|
||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class EventCapability extends Capability {
|
public class EventCapability extends Capability {
|
||||||
private Optional<NetatmoServlet> servlet = Optional.empty();
|
private Optional<WebhookServlet> webhook = Optional.empty();
|
||||||
|
|
||||||
public EventCapability(CommonInterface handler) {
|
public EventCapability(CommonInterface handler) {
|
||||||
super(handler);
|
super(handler);
|
||||||
|
@ -39,13 +38,13 @@ public class EventCapability extends Capability {
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
ApiBridgeHandler accountHandler = handler.getAccountHandler();
|
ApiBridgeHandler accountHandler = handler.getAccountHandler();
|
||||||
if (accountHandler != null) {
|
if (accountHandler != null) {
|
||||||
servlet = accountHandler.getServlet();
|
webhook = accountHandler.getWebHookServlet();
|
||||||
servlet.ifPresent(s -> s.registerDataListener(handler.getId(), this));
|
webhook.ifPresent(servlet -> servlet.registerDataListener(handler.getId(), this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
servlet.ifPresent(s -> s.unregisterDataListener(this));
|
webhook.ifPresent(servlet -> servlet.unregisterDataListener(handler.getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.util.stream.Collectors;
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
|
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
|
||||||
|
import org.openhab.binding.netatmo.internal.config.NAThingConfiguration;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
import org.openhab.core.thing.binding.ThingTypeProvider;
|
import org.openhab.core.thing.binding.ThingTypeProvider;
|
||||||
import org.openhab.core.thing.i18n.ThingTypeI18nLocalizationService;
|
import org.openhab.core.thing.i18n.ThingTypeI18nLocalizationService;
|
||||||
|
@ -73,7 +74,8 @@ public class NetatmoThingTypeProvider implements ThingTypeProvider {
|
||||||
ModuleType moduleType = ModuleType.from(thingTypeUID);
|
ModuleType moduleType = ModuleType.from(thingTypeUID);
|
||||||
|
|
||||||
ThingTypeBuilder thingTypeBuilder = ThingTypeBuilder.instance(thingTypeUID, thingTypeUID.toString())
|
ThingTypeBuilder thingTypeBuilder = ThingTypeBuilder.instance(thingTypeUID, thingTypeUID.toString())
|
||||||
.withRepresentationProperty(EQUIPMENT_ID).withExtensibleChannelTypeIds(moduleType.extensions)
|
.withRepresentationProperty(NAThingConfiguration.ID)
|
||||||
|
.withExtensibleChannelTypeIds(moduleType.extensions)
|
||||||
.withChannelGroupDefinitions(getGroupDefinitions(moduleType))
|
.withChannelGroupDefinitions(getGroupDefinitions(moduleType))
|
||||||
.withConfigDescriptionURI(moduleType.getConfigDescription());
|
.withConfigDescriptionURI(moduleType.getConfigDescription());
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
/**
|
||||||
|
* 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.netatmo.internal.servlet;
|
||||||
|
|
||||||
|
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.PARAM_ERROR;
|
||||||
|
import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
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.util.MultiMap;
|
||||||
|
import org.eclipse.jetty.util.UrlEncoded;
|
||||||
|
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
||||||
|
import org.osgi.service.http.HttpService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link GrantServlet} manages the authorization with the Netatmo API. The servlet implements the
|
||||||
|
* Authorization Code flow and saves the resulting refreshToken with the bridge.
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class GrantServlet extends NetatmoServlet {
|
||||||
|
private static final long serialVersionUID = 4817341543768441689L;
|
||||||
|
private static final Pattern MESSAGE_KEY_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}");
|
||||||
|
private static final String TEMPLATE_ACCOUNT = "template/account.html";
|
||||||
|
private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
|
||||||
|
|
||||||
|
// Simple HTML templates for inserting messages.
|
||||||
|
private static final String HTML_ERROR = "<p class='block error'>Call to Netatmo Connect failed with error: %s</p>";
|
||||||
|
|
||||||
|
// Keys present in the account.html
|
||||||
|
private static final String KEY_ERROR = "error";
|
||||||
|
private static final String ACCOUNT_NAME = "account.name";
|
||||||
|
private static final String ACCOUNT_AUTHORIZED_CLASS = "account.authorized";
|
||||||
|
private static final String ACCOUNT_AUTHORIZE = "account.authorize";
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(GrantServlet.class);
|
||||||
|
private final @NonNullByDefault({}) ClassLoader classLoader = GrantServlet.class.getClassLoader();
|
||||||
|
private final String accountTemplate;
|
||||||
|
|
||||||
|
public GrantServlet(ApiBridgeHandler handler, HttpService httpService) {
|
||||||
|
super(handler, httpService, "connect");
|
||||||
|
try (InputStream stream = classLoader.getResourceAsStream(TEMPLATE_ACCOUNT)) {
|
||||||
|
accountTemplate = stream != null ? new String(stream.readAllBytes(), StandardCharsets.UTF_8) : "";
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException("Unable to load template account file. Please file a bug report.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||||
|
logger.debug("Netatmo auth callback servlet received GET request {}.", req.getRequestURI());
|
||||||
|
StringBuffer requestUrl = req.getRequestURL();
|
||||||
|
if (requestUrl != null) {
|
||||||
|
final String servletBaseURL = requestUrl.toString();
|
||||||
|
final Map<String, String> replaceMap = new HashMap<>();
|
||||||
|
|
||||||
|
handleRedirect(replaceMap, servletBaseURL, req.getQueryString());
|
||||||
|
|
||||||
|
String label = handler.getThing().getLabel();
|
||||||
|
replaceMap.put(ACCOUNT_NAME, label != null ? label : "");
|
||||||
|
replaceMap.put(CLIENT_ID, handler.getId());
|
||||||
|
replaceMap.put(ACCOUNT_AUTHORIZED_CLASS, handler.isConnected() ? " authorized" : " Unauthorized");
|
||||||
|
replaceMap.put(ACCOUNT_AUTHORIZE,
|
||||||
|
handler.formatAuthorizationUrl().queryParam(REDIRECT_URI, servletBaseURL).build().toString());
|
||||||
|
replaceMap.put(REDIRECT_URI, servletBaseURL);
|
||||||
|
|
||||||
|
resp.setContentType(CONTENT_TYPE);
|
||||||
|
resp.getWriter().append(replaceKeysFromMap(accountTemplate, replaceMap));
|
||||||
|
resp.getWriter().close();
|
||||||
|
} else {
|
||||||
|
logger.warn("Unexpected : requestUrl is null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a possible call from Netatmo to the redirect_uri. If that is the case it will pass the authorization
|
||||||
|
* codes via the url and these are processed. In case of an error this is shown to the user. If the user was
|
||||||
|
* authorized this is passed on to the handler. Based on all these different outcomes the HTML is generated to
|
||||||
|
* inform the user.
|
||||||
|
*
|
||||||
|
* @param replaceMap a map with key String values that will be mapped in the HTML templates.
|
||||||
|
* @param servletBaseURL the servlet base, which should be used as the redirect_uri value
|
||||||
|
* @param queryString the query part of the GET request this servlet is processing
|
||||||
|
*/
|
||||||
|
private void handleRedirect(Map<String, String> replaceMap, String servletBaseURL, @Nullable String queryString) {
|
||||||
|
replaceMap.put(KEY_ERROR, "");
|
||||||
|
|
||||||
|
if (queryString != null) {
|
||||||
|
final MultiMap<@Nullable String> params = new MultiMap<>();
|
||||||
|
UrlEncoded.decodeTo(queryString, params, StandardCharsets.UTF_8.name());
|
||||||
|
final String reqCode = params.getString(CODE);
|
||||||
|
final String reqState = params.getString(STATE);
|
||||||
|
final String reqError = params.getString(PARAM_ERROR);
|
||||||
|
|
||||||
|
if (reqError != null) {
|
||||||
|
logger.debug("Netatmo redirected with an error: {}", reqError);
|
||||||
|
replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, reqError));
|
||||||
|
} else if (reqState != null && reqCode != null) {
|
||||||
|
handler.openConnection(reqCode, servletBaseURL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all keys from the map found in the template with values from the map. If the key is not found the key
|
||||||
|
* will be kept in the template.
|
||||||
|
*
|
||||||
|
* @param template template to replace keys with values
|
||||||
|
* @param map map with key value pairs to replace in the template
|
||||||
|
* @return a template with keys replaced
|
||||||
|
*/
|
||||||
|
private String replaceKeysFromMap(String template, Map<String, String> map) {
|
||||||
|
final Matcher m = MESSAGE_KEY_PATTERN.matcher(template);
|
||||||
|
final StringBuffer sb = new StringBuffer();
|
||||||
|
|
||||||
|
while (m.find()) {
|
||||||
|
try {
|
||||||
|
final String key = m.group(1);
|
||||||
|
m.appendReplacement(sb, Matcher.quoteReplacement(map.getOrDefault(key, "${" + key + '}')));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
logger.debug("Error occurred during template filling, cause ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.appendTail(sb);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* 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.netatmo.internal.servlet;
|
||||||
|
|
||||||
|
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.BINDING_ID;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
||||||
|
import org.osgi.service.http.HttpService;
|
||||||
|
import org.osgi.service.http.NamespaceException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link NetatmoServlet} is the ancestor class for Netatmo servlets
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public abstract class NetatmoServlet extends HttpServlet {
|
||||||
|
private static final long serialVersionUID = 5671438863935117735L;
|
||||||
|
private static final String BASE_PATH = "/" + BINDING_ID + "/";
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
private final HttpService httpService;
|
||||||
|
|
||||||
|
protected final ApiBridgeHandler handler;
|
||||||
|
protected final String path;
|
||||||
|
|
||||||
|
public NetatmoServlet(ApiBridgeHandler handler, HttpService httpService, String localPath) {
|
||||||
|
this.path = BASE_PATH + localPath + "/" + handler.getId();
|
||||||
|
this.handler = handler;
|
||||||
|
this.httpService = httpService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startListening() {
|
||||||
|
try {
|
||||||
|
httpService.registerServlet(path, this, null, httpService.createDefaultHttpContext());
|
||||||
|
logger.info("Registered Netatmo servlet at '{}'", path);
|
||||||
|
} catch (NamespaceException | ServletException e) {
|
||||||
|
logger.warn("Registering servlet failed:{}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
logger.debug("Stopping Netatmo Servlet {}", path);
|
||||||
|
httpService.unregister(path);
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
/**
|
||||||
|
* 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.netatmo.internal.servlet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Scanner;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.ws.rs.HttpMethod;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import javax.ws.rs.core.UriBuilderException;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.binding.netatmo.internal.api.NetatmoException;
|
||||||
|
import org.openhab.binding.netatmo.internal.api.SecurityApi;
|
||||||
|
import org.openhab.binding.netatmo.internal.api.dto.WebhookEvent;
|
||||||
|
import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
|
||||||
|
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
||||||
|
import org.openhab.binding.netatmo.internal.handler.capability.EventCapability;
|
||||||
|
import org.osgi.service.http.HttpService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP servlet for Netatmo Webhook.
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class WebhookServlet extends NetatmoServlet {
|
||||||
|
private static final long serialVersionUID = -354583910860541214L;
|
||||||
|
|
||||||
|
private final Map<String, EventCapability> dataListeners = new ConcurrentHashMap<>();
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(WebhookServlet.class);
|
||||||
|
private final SecurityApi securityApi;
|
||||||
|
private final NADeserializer deserializer;
|
||||||
|
private final String webHookUrl;
|
||||||
|
|
||||||
|
private boolean hookSet = false;
|
||||||
|
|
||||||
|
public WebhookServlet(ApiBridgeHandler handler, HttpService httpService, NADeserializer deserializer,
|
||||||
|
SecurityApi securityApi, String webHookUrl) {
|
||||||
|
super(handler, httpService, "webhook");
|
||||||
|
this.deserializer = deserializer;
|
||||||
|
this.securityApi = securityApi;
|
||||||
|
this.webHookUrl = webHookUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startListening() {
|
||||||
|
super.startListening();
|
||||||
|
URI uri = UriBuilder.fromUri(webHookUrl).path(path).build();
|
||||||
|
try {
|
||||||
|
logger.info("Setting up WebHook at Netatmo to {}", uri.toString());
|
||||||
|
hookSet = securityApi.addwebhook(uri);
|
||||||
|
} catch (UriBuilderException e) {
|
||||||
|
logger.info("webhookUrl is not a valid URI '{}' : {}", uri, e.getMessage());
|
||||||
|
} catch (NetatmoException e) {
|
||||||
|
logger.info("Error setting webhook : {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
if (hookSet) {
|
||||||
|
logger.info("Releasing WebHook at Netatmo ");
|
||||||
|
try {
|
||||||
|
securityApi.dropWebhook();
|
||||||
|
hookSet = false;
|
||||||
|
} catch (NetatmoException e) {
|
||||||
|
logger.warn("Error releasing webhook : {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
|
||||||
|
replyQuick(resp);
|
||||||
|
processEvent(inputStreamToString(req.getInputStream()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processEvent(String data) throws IOException {
|
||||||
|
if (!data.isEmpty()) {
|
||||||
|
logger.debug("Event transmitted from restService : {}", data);
|
||||||
|
try {
|
||||||
|
WebhookEvent event = deserializer.deserialize(WebhookEvent.class, data);
|
||||||
|
List<String> toBeNotified = new ArrayList<>();
|
||||||
|
toBeNotified.add(event.getCameraId());
|
||||||
|
toBeNotified.addAll(event.getPersons().keySet());
|
||||||
|
notifyListeners(toBeNotified, event);
|
||||||
|
} catch (NetatmoException e) {
|
||||||
|
logger.debug("Error deserializing webhook data received : {}. {}", data, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replyQuick(HttpServletResponse resp) throws IOException {
|
||||||
|
resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
resp.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
resp.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
resp.setHeader("Access-Control-Allow-Methods", HttpMethod.POST);
|
||||||
|
resp.setIntHeader("Access-Control-Max-Age", 3600);
|
||||||
|
resp.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||||
|
resp.getWriter().write("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String inputStreamToString(InputStream is) throws IOException {
|
||||||
|
String value = "";
|
||||||
|
try (Scanner scanner = new Scanner(is)) {
|
||||||
|
scanner.useDelimiter("\\A");
|
||||||
|
value = scanner.hasNext() ? scanner.next() : "";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyListeners(List<String> tobeNotified, WebhookEvent event) {
|
||||||
|
tobeNotified.forEach(id -> {
|
||||||
|
EventCapability module = dataListeners.get(id);
|
||||||
|
if (module != null) {
|
||||||
|
module.setNewData(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerDataListener(String id, EventCapability eventCapability) {
|
||||||
|
dataListeners.put(id, eventCapability);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterDataListener(String id) {
|
||||||
|
dataListeners.remove(id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,163 +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.netatmo.internal.webhook;
|
|
||||||
|
|
||||||
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.BINDING_ID;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Scanner;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServlet;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import javax.ws.rs.HttpMethod;
|
|
||||||
import javax.ws.rs.core.MediaType;
|
|
||||||
import javax.ws.rs.core.UriBuilder;
|
|
||||||
import javax.ws.rs.core.UriBuilderException;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
|
||||||
import org.openhab.binding.netatmo.internal.api.NetatmoException;
|
|
||||||
import org.openhab.binding.netatmo.internal.api.SecurityApi;
|
|
||||||
import org.openhab.binding.netatmo.internal.api.dto.WebhookEvent;
|
|
||||||
import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
|
|
||||||
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
|
||||||
import org.openhab.binding.netatmo.internal.handler.capability.EventCapability;
|
|
||||||
import org.osgi.service.http.HttpService;
|
|
||||||
import org.osgi.service.http.NamespaceException;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP servlet for Netatmo Webhook.
|
|
||||||
*
|
|
||||||
* @author Gaël L'hopital - Initial contribution
|
|
||||||
*/
|
|
||||||
@NonNullByDefault
|
|
||||||
public class NetatmoServlet extends HttpServlet {
|
|
||||||
private static final long serialVersionUID = -354583910860541214L;
|
|
||||||
private static final String CALLBACK_URI = "/" + BINDING_ID;
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(NetatmoServlet.class);
|
|
||||||
private final Map<String, EventCapability> dataListeners = new ConcurrentHashMap<>();
|
|
||||||
private final HttpService httpService;
|
|
||||||
private final NADeserializer deserializer;
|
|
||||||
private final Optional<SecurityApi> securityApi;
|
|
||||||
private boolean hookSet = false;
|
|
||||||
|
|
||||||
public NetatmoServlet(HttpService httpService, ApiBridgeHandler apiBridge, String webHookUrl) {
|
|
||||||
this.httpService = httpService;
|
|
||||||
this.deserializer = apiBridge.getDeserializer();
|
|
||||||
this.securityApi = Optional.ofNullable(apiBridge.getRestManager(SecurityApi.class));
|
|
||||||
securityApi.ifPresent(api -> {
|
|
||||||
try {
|
|
||||||
httpService.registerServlet(CALLBACK_URI, this, null, httpService.createDefaultHttpContext());
|
|
||||||
logger.debug("Started Netatmo Webhook Servlet at '{}'", CALLBACK_URI);
|
|
||||||
URI uri = UriBuilder.fromUri(webHookUrl).path(BINDING_ID).build();
|
|
||||||
try {
|
|
||||||
logger.info("Setting Netatmo Welcome WebHook to {}", uri.toString());
|
|
||||||
api.addwebhook(uri);
|
|
||||||
hookSet = true;
|
|
||||||
} catch (UriBuilderException e) {
|
|
||||||
logger.info("webhookUrl is not a valid URI '{}' : {}", uri, e.getMessage());
|
|
||||||
} catch (NetatmoException e) {
|
|
||||||
logger.info("Error setting webhook : {}", e.getMessage());
|
|
||||||
}
|
|
||||||
} catch (ServletException | NamespaceException e) {
|
|
||||||
logger.warn("Could not start Netatmo Webhook Servlet : {}", e.getMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dispose() {
|
|
||||||
securityApi.ifPresent(api -> {
|
|
||||||
if (hookSet) {
|
|
||||||
logger.info("Releasing Netatmo Welcome WebHook");
|
|
||||||
try {
|
|
||||||
api.dropWebhook();
|
|
||||||
} catch (NetatmoException e) {
|
|
||||||
logger.warn("Error releasing webhook : {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
httpService.unregister(CALLBACK_URI);
|
|
||||||
});
|
|
||||||
logger.debug("Netatmo Webhook Servlet stopped");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void service(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
|
|
||||||
if (req != null && resp != null) {
|
|
||||||
String data = inputStreamToString(req.getInputStream());
|
|
||||||
if (!data.isEmpty()) {
|
|
||||||
logger.debug("Event transmitted from restService : {}", data);
|
|
||||||
try {
|
|
||||||
WebhookEvent event = deserializer.deserialize(WebhookEvent.class, data);
|
|
||||||
List<String> tobeNotified = collectNotified(event);
|
|
||||||
dataListeners.keySet().stream().filter(tobeNotified::contains).forEach(id -> {
|
|
||||||
EventCapability module = dataListeners.get(id);
|
|
||||||
if (module != null) {
|
|
||||||
module.setNewData(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (NetatmoException e) {
|
|
||||||
logger.info("Error deserializing webhook data received : {}. {}", data, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
|
||||||
resp.setContentType(MediaType.APPLICATION_JSON);
|
|
||||||
resp.setHeader("Access-Control-Allow-Origin", "*");
|
|
||||||
resp.setHeader("Access-Control-Allow-Methods", HttpMethod.POST);
|
|
||||||
resp.setIntHeader("Access-Control-Max-Age", 3600);
|
|
||||||
resp.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
|
||||||
resp.getWriter().write("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> collectNotified(WebhookEvent event) {
|
|
||||||
List<String> result = new ArrayList<>();
|
|
||||||
result.add(event.getCameraId());
|
|
||||||
String person = event.getPersonId();
|
|
||||||
if (person != null) {
|
|
||||||
result.add(person);
|
|
||||||
}
|
|
||||||
result.addAll(event.getPersons().keySet());
|
|
||||||
return result.stream().distinct().collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void registerDataListener(String id, EventCapability dataListener) {
|
|
||||||
dataListeners.put(id, dataListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unregisterDataListener(EventCapability dataListener) {
|
|
||||||
dataListeners.entrySet().removeIf(entry -> entry.getValue().equals(dataListener));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String inputStreamToString(InputStream is) throws IOException {
|
|
||||||
String value = "";
|
|
||||||
try (Scanner scanner = new Scanner(is)) {
|
|
||||||
scanner.useDelimiter("\\A");
|
|
||||||
value = scanner.hasNext() ? scanner.next() : "";
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,20 +17,16 @@
|
||||||
<context>password</context>
|
<context>password</context>
|
||||||
</parameter>
|
</parameter>
|
||||||
|
|
||||||
<parameter name="username" type="text" required="true">
|
<parameter name="refreshToken" type="text">
|
||||||
<label>Username</label>
|
<label>Refresh Token</label>
|
||||||
<description>Your Netatmo API username (email).</description>
|
<description>Refresh token provided by the oAuth2 authentication process.</description>
|
||||||
</parameter>
|
|
||||||
|
|
||||||
<parameter name="password" type="text" required="true">
|
|
||||||
<label>Password</label>
|
|
||||||
<description>Your Netatmo API password.</description>
|
|
||||||
<context>password</context>
|
<context>password</context>
|
||||||
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</parameter>
|
||||||
|
|
||||||
<parameter name="webHookUrl" type="text" required="false">
|
<parameter name="webHookUrl" type="text" required="false">
|
||||||
<label>Webhook Address</label>
|
<label>Webhook Address</label>
|
||||||
<description>Protocol, public IP and port to access openHAB server from Internet.</description>
|
<description>Protocol, public IP or hostname and port to access openHAB server from Internet.</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
|
|
||||||
<parameter name="reconnectInterval" type="integer" unit="s">
|
<parameter name="reconnectInterval" type="integer" unit="s">
|
||||||
|
|
|
@ -330,8 +330,7 @@ thing-type.netatmo.wind.description = Wind sensor reporting wind angle and stren
|
||||||
|
|
||||||
conf-error-no-client-id = Cannot connect to Netatmo bridge as no client id is available in the configuration
|
conf-error-no-client-id = Cannot connect to Netatmo bridge as no client id is available in the configuration
|
||||||
conf-error-no-client-secret = Cannot connect to Netatmo bridge as no client secret is available in the configuration
|
conf-error-no-client-secret = Cannot connect to Netatmo bridge as no client secret is available in the configuration
|
||||||
conf-error-no-username = Cannot connect to Netatmo bridge as no username is available in the configuration
|
conf-error-grant-needed = Configuration incomplete, please grant the binding to Netatmo Connect.
|
||||||
conf-error-no-password = Cannot connect to Netatmo bridge as no password is available in the configuration
|
|
||||||
status-bridge-offline = Bridge is not connected to Netatmo API
|
status-bridge-offline = Bridge is not connected to Netatmo API
|
||||||
device-not-connected = Thing is not reachable
|
device-not-connected = Thing is not reachable
|
||||||
data-over-limit = Data seems quite old
|
data-over-limit = Data seems quite old
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>Authorize openHAB Bridge at Netatmo Connect</title>
|
||||||
|
<link>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
font-family: "Roboto", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
background-color: white;
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #FFC0C0;
|
||||||
|
border: 1px solid darkred;
|
||||||
|
color: darkred
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorized {
|
||||||
|
border: 1px solid #90EE90;
|
||||||
|
background-color: #E0FFE0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button a {
|
||||||
|
background: #1ED760;
|
||||||
|
border-radius: 500px;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-width: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>Authorize openHAB Bridge at Netatmo Connect</h3>
|
||||||
|
<p>On this page you can authorize your openHAB Netatmo Bridge configured with the clientId and clientSecret of the Netatmo Application on your Developer account.</p>
|
||||||
|
<p>You have to login to your Netatmo Account and authorize this binding to access your account.</p>
|
||||||
|
<p>To use this binding the following requirements apply:</p>
|
||||||
|
<ul>
|
||||||
|
<li>A Netatmo connect account.
|
||||||
|
<li>Register openHAB as an App on your Netatmo Connect account.
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
The redirect URI to use with Netatmo for this openHAB Netatmo Bridge is
|
||||||
|
<a href="${redirect_uri}">${redirect_uri}</a>
|
||||||
|
</p>
|
||||||
|
${error}
|
||||||
|
<div class="block${account.authorized}" id="${client_id}">
|
||||||
|
Connect to Netatmo: <i>${account.name}</i>
|
||||||
|
<p><div class="button"><a href=${account.authorize}>Authorize Thing</a></div></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue