[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
|
||||
|
||||
Before setting up your 'Things', you will have to grant openHAB to access Netatmo API.
|
||||
Here is the procedure:
|
||||
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.
|
||||
|
||||
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_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:
|
||||
|
||||
|
@ -31,18 +34,34 @@ The binding has the following configuration options:
|
|||
| 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.
|
||||
|
||||
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.
|
||||
- **clientSecret:** Client Secret provided for the application you created.
|
||||
- **username:** Your Netatmo API username (email).
|
||||
- **password:** Your Netatmo API password.
|
||||
- **webHookUrl:** Protocol, public IP and port to access openHAB server from Internet.
|
||||
- **reconnectInterval:** The reconnection interval to Netatmo API (in s).
|
||||
| Parameter | Type | Required | Description |
|
||||
|-------------------|--------|----------|------------------------------------------------------------------------------------------------------------------------|
|
||||
| clientId | String | Yes | Client ID provided for the application you created on http://dev.netatmo.com/createapp |
|
||||
| clientSecret | String | Yes | Client Secret provided for the application you created |
|
||||
| webHookUrl | String | No | Protocol, public IP and port to access openHAB server from Internet |
|
||||
| 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
|
||||
|
@ -73,7 +92,7 @@ The Account bridge has the following configuration options:
|
|||
### Webhook
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
|
@ -83,7 +102,7 @@ http(s)://xx.yy.zz.ww:443
|
|||
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.
|
||||
|
@ -519,7 +538,7 @@ All these channels except at-home are read only.
|
|||
## 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"] {
|
||||
outdoor outside "Outside Module" [id="02:00:00:aa:aa:aa"] {
|
||||
Channels:
|
||||
|
|
|
@ -27,9 +27,6 @@ public class NetatmoBindingConstants {
|
|||
public static final String BINDING_ID = "netatmo";
|
||||
public static final String VENDOR = "Netatmo";
|
||||
|
||||
// Configuration keys
|
||||
public static final String EQUIPMENT_ID = "id";
|
||||
|
||||
// Things properties
|
||||
public static final String PROPERTY_CITY = "city";
|
||||
public static final String PROPERTY_COUNTRY = "country";
|
||||
|
|
|
@ -68,11 +68,11 @@ import org.slf4j.LoggerFactory;
|
|||
public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
|
||||
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 NetatmoDescriptionProvider stateDescriptionProvider;
|
||||
private final NADeserializer deserializer;
|
||||
private final HttpClient httpClient;
|
||||
private final HttpService httpService;
|
||||
|
||||
@Activate
|
||||
public NetatmoHandlerFactory(@Reference NetatmoDescriptionProvider stateDescriptionProvider,
|
||||
|
@ -80,8 +80,8 @@ public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
|
|||
@Reference HttpService httpService, Map<String, @Nullable Object> config) {
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
this.httpClient = factory.getCommonHttpClient();
|
||||
this.httpService = httpService;
|
||||
this.deserializer = deserializer;
|
||||
this.httpService = httpService;
|
||||
configChanged(config);
|
||||
}
|
||||
|
||||
|
@ -107,7 +107,7 @@ public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
|
|||
|
||||
private BaseThingHandler buildHandler(Thing thing, ModuleType 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);
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
*/
|
||||
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 java.net.URI;
|
||||
|
@ -24,12 +24,14 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
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.Scope;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -41,41 +43,57 @@ import org.slf4j.LoggerFactory;
|
|||
*/
|
||||
@NonNullByDefault
|
||||
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 ScheduledExecutorService scheduler;
|
||||
|
||||
private @Nullable ScheduledFuture<?> refreshTokenJob;
|
||||
private Optional<ScheduledFuture<?>> refreshTokenJob = Optional.empty();
|
||||
private Optional<AccessTokenResponse> tokenResponse = Optional.empty();
|
||||
private String scope = "";
|
||||
|
||||
public AuthenticationApi(ApiBridgeHandler bridge, ScheduledExecutorService scheduler) {
|
||||
super(bridge, FeatureArea.NONE);
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
public void authenticate(Credentials credentials, Set<FeatureArea> features) throws NetatmoException {
|
||||
Set<FeatureArea> requestedFeatures = !features.isEmpty() ? features : FeatureArea.AS_SET;
|
||||
scope = FeatureArea.toScopeString(requestedFeatures);
|
||||
requestToken(credentials.clientId, credentials.clientSecret,
|
||||
Map.of(USERNAME, credentials.username, PASSWORD, credentials.password, SCOPE, scope));
|
||||
public String authorize(ApiHandlerConfiguration credentials, Set<FeatureArea> features, @Nullable String code,
|
||||
@Nullable String redirectUri) throws NetatmoException {
|
||||
String clientId = credentials.clientId;
|
||||
String clientSecret = credentials.clientSecret;
|
||||
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);
|
||||
payload.putAll(Map.of(GRANT_TYPE, entries.keySet().contains(PASSWORD) ? PASSWORD : REFRESH_TOKEN, CLIENT_ID, id,
|
||||
CLIENT_SECRET, secret));
|
||||
payload.put(GRANT_TYPE, payload.keySet().contains(CODE) ? AUTHORIZATION_CODE : REFRESH_TOKEN);
|
||||
payload.putAll(Map.of(CLIENT_ID, id, CLIENT_SECRET, secret));
|
||||
disconnect();
|
||||
AccessTokenResponse response = post(OAUTH_URI, AccessTokenResponse.class, payload);
|
||||
refreshTokenJob = scheduler.schedule(() -> {
|
||||
AccessTokenResponse response = post(TOKEN_URI, AccessTokenResponse.class, payload);
|
||||
refreshTokenJob = Optional.of(scheduler.schedule(() -> {
|
||||
try {
|
||||
requestToken(id, secret, Map.of(REFRESH_TOKEN, response.getRefreshToken()));
|
||||
} catch (NetatmoException e) {
|
||||
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);
|
||||
return response.getRefreshToken();
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
|
@ -83,11 +101,8 @@ public class AuthenticationApi extends RestManager {
|
|||
}
|
||||
|
||||
public void dispose() {
|
||||
ScheduledFuture<?> job = refreshTokenJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
refreshTokenJob = null;
|
||||
refreshTokenJob.ifPresent(job -> job.cancel(true));
|
||||
refreshTokenJob = Optional.empty();
|
||||
}
|
||||
|
||||
public @Nullable String getAuthorization() {
|
||||
|
@ -95,12 +110,20 @@ public class AuthenticationApi extends RestManager {
|
|||
}
|
||||
|
||||
public boolean matchesScopes(Set<Scope> requiredScopes) {
|
||||
// either we do not require any scope, either connected and all scopes available
|
||||
return requiredScopes.isEmpty()
|
||||
return requiredScopes.isEmpty() // either we do not require any scope, either connected and all scopes available
|
||||
|| (isConnected() && tokenResponse.map(at -> at.getScope().containsAll(requiredScopes)).orElse(false));
|
||||
}
|
||||
|
||||
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() {
|
||||
String message = super.getMessage();
|
||||
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)
|
||||
* @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());
|
||||
post(uriBuilder, ApiResponse.Ok.class, null, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Collection<HomeEvent> getPersonEvents(String homeId, String personId) throws NetatmoException {
|
||||
|
|
|
@ -116,7 +116,9 @@ public class NetatmoConstants {
|
|||
// Netatmo API urls
|
||||
public static final String URL_API = "https://api.netatmo.com/";
|
||||
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_COMMAND = "command";
|
||||
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_STATUS = "status";
|
||||
|
||||
// Autentication process params
|
||||
public static final String PARAM_ERROR = "error";
|
||||
|
||||
// Global variables
|
||||
public static final int THERM_MAX_SETPOINT = 30;
|
||||
|
||||
|
|
|
@ -13,8 +13,6 @@
|
|||
package org.openhab.binding.netatmo.internal.config;
|
||||
|
||||
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
|
||||
|
@ -24,39 +22,23 @@ import org.openhab.binding.netatmo.internal.api.NetatmoException;
|
|||
*/
|
||||
@NonNullByDefault
|
||||
public class ApiHandlerConfiguration {
|
||||
public class Credentials {
|
||||
public final String clientId, clientSecret, username, password;
|
||||
public static final String CLIENT_ID = "clientId";
|
||||
public static final String REFRESH_TOKEN = "refreshToken";
|
||||
|
||||
private Credentials(@Nullable String clientId, @Nullable String clientSecret, @Nullable String username,
|
||||
@Nullable String password) throws NetatmoException {
|
||||
this.clientSecret = checkMandatory(clientSecret, "@text/conf-error-no-client-secret");
|
||||
this.username = checkMandatory(username, "@text/conf-error-no-username");
|
||||
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 String clientId = "";
|
||||
public String clientSecret = "";
|
||||
public String refreshToken = "";
|
||||
public String webHookUrl = "";
|
||||
public int reconnectInterval = 300;
|
||||
|
||||
public Credentials getCredentials() throws NetatmoException {
|
||||
return new Credentials(clientId, clientSecret, username, password);
|
||||
public ConfigurationLevel check() {
|
||||
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
|
||||
public class NAThingConfiguration {
|
||||
public static final String ID = "id";
|
||||
|
||||
public String id = "";
|
||||
public int refreshInterval = -1;
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@
|
|||
*/
|
||||
package org.openhab.binding.netatmo.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.EQUIPMENT_ID;
|
||||
|
||||
import java.util.Set;
|
||||
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.dto.NAMain;
|
||||
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.core.config.discovery.AbstractDiscoveryService;
|
||||
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 final Logger logger = LoggerFactory.getLogger(NetatmoDiscoveryService.class);
|
||||
private @Nullable ApiBridgeHandler handler;
|
||||
private @Nullable BindingConfiguration config;
|
||||
private boolean readFriends;
|
||||
|
||||
public NetatmoDiscoveryService() {
|
||||
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
|
||||
public void startScan() {
|
||||
BindingConfiguration localConf = config;
|
||||
ApiBridgeHandler localHandler = handler;
|
||||
if (localHandler != null && localConf != null) {
|
||||
if (localHandler != null) {
|
||||
ThingUID apiBridgeUID = localHandler.getThing().getUID();
|
||||
try {
|
||||
AircareApi airCareApi = localHandler.getRestManager(AircareApi.class);
|
||||
|
@ -73,7 +70,7 @@ public class NetatmoDiscoveryService extends AbstractDiscoveryService implements
|
|||
body.getElements().stream().forEach(homeCoach -> createThing(homeCoach, apiBridgeUID));
|
||||
}
|
||||
}
|
||||
if (localConf.readFriends) {
|
||||
if (readFriends) {
|
||||
WeatherApi weatherApi = localHandler.getRestManager(WeatherApi.class);
|
||||
if (weatherApi != null) { // Search favorite stations
|
||||
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) {
|
||||
ThingUID moduleUID = findThingUID(module.getType(), module.getId(), bridgeUID);
|
||||
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());
|
||||
if (bridgeUID != null) {
|
||||
resultBuilder.withBridge(bridgeUID);
|
||||
|
@ -140,7 +138,7 @@ public class NetatmoDiscoveryService extends AbstractDiscoveryService implements
|
|||
public void setThingHandler(ThingHandler handler) {
|
||||
if (handler instanceof ApiBridgeHandler) {
|
||||
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.TimeoutException;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.NetatmoException;
|
||||
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.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.ConfigurationLevel;
|
||||
import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
|
||||
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.ChannelUID;
|
||||
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 BindingConfiguration bindingConf;
|
||||
private final HttpService httpService;
|
||||
private final AuthenticationApi connectApi;
|
||||
private final HttpClient httpClient;
|
||||
private final NADeserializer deserializer;
|
||||
private final HttpService httpService;
|
||||
|
||||
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 @Nullable WebhookServlet webHookServlet;
|
||||
private @Nullable GrantServlet grantServlet;
|
||||
|
||||
public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, HttpService httpService, NADeserializer deserializer,
|
||||
BindingConfiguration configuration) {
|
||||
public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, NADeserializer deserializer,
|
||||
BindingConfiguration configuration, HttpService httpService) {
|
||||
super(bridge);
|
||||
this.bindingConf = configuration;
|
||||
this.httpService = httpService;
|
||||
this.connectApi = new AuthenticationApi(this, scheduler);
|
||||
this.httpClient = httpClient;
|
||||
this.deserializer = deserializer;
|
||||
this.httpService = httpService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing Netatmo API bridge handler.");
|
||||
thingConf = getConfigAs(ApiHandlerConfiguration.class);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
scheduler.execute(() -> {
|
||||
openConnection();
|
||||
String webHookUrl = thingConf.webHookUrl;
|
||||
if (webHookUrl != null && !webHookUrl.isBlank()) {
|
||||
servlet = Optional.of(new NetatmoServlet(httpService, this, webHookUrl));
|
||||
}
|
||||
});
|
||||
scheduler.execute(() -> openConnection(null, null));
|
||||
}
|
||||
|
||||
private void openConnection() {
|
||||
try {
|
||||
Credentials credentials = thingConf.getCredentials();
|
||||
logger.debug("Connecting to Netatmo API.");
|
||||
try {
|
||||
connectApi.authenticate(credentials, bindingConf.features);
|
||||
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();
|
||||
}
|
||||
} catch (NetatmoException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
public void openConnection(@Nullable String code, @Nullable String redirectUri) {
|
||||
ApiHandlerConfiguration configuration = getConfiguration();
|
||||
ConfigurationLevel level = configuration.check();
|
||||
switch (level) {
|
||||
case EMPTY_CLIENT_ID:
|
||||
case EMPTY_CLIENT_SECRET:
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, level.message);
|
||||
break;
|
||||
case REFRESH_TOKEN_NEEDED:
|
||||
if (code == null || redirectUri == null) {
|
||||
GrantServlet servlet = new GrantServlet(this, httpService);
|
||||
servlet.startListening();
|
||||
this.grantServlet = servlet;
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, level.message);
|
||||
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();
|
||||
freeConnectJob();
|
||||
connectJob = Optional
|
||||
.of(scheduler.schedule(() -> openConnection(), thingConf.reconnectInterval, TimeUnit.SECONDS));
|
||||
connectJob = Optional.of(scheduler.schedule(() -> openConnection(code, redirectUri),
|
||||
getConfiguration().reconnectInterval, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
private void freeConnectJob() {
|
||||
|
@ -141,8 +177,14 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Shutting down Netatmo API bridge handler.");
|
||||
servlet.ifPresent(servlet -> servlet.dispose());
|
||||
servlet = Optional.empty();
|
||||
WebhookServlet localWebHook = this.webHookServlet;
|
||||
if (localWebHook != null) {
|
||||
localWebHook.dispose();
|
||||
}
|
||||
GrantServlet localGrant = this.grantServlet;
|
||||
if (localGrant != null) {
|
||||
localGrant.dispose();
|
||||
}
|
||||
connectApi.dispose();
|
||||
freeConnectJob();
|
||||
super.dispose();
|
||||
|
@ -153,11 +195,6 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||
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")
|
||||
public <T extends RestManager> @Nullable T getRestManager(Class<T> clazz) {
|
||||
if (!managers.containsKey(clazz)) {
|
||||
|
@ -218,24 +255,33 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||
return executeUri(uri, method, clazz, payload, contentType, retryCount - 1);
|
||||
}
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
public BindingConfiguration getConfiguration() {
|
||||
return bindingConf;
|
||||
}
|
||||
|
||||
public Optional<NetatmoServlet> getServlet() {
|
||||
return servlet;
|
||||
}
|
||||
|
||||
public NADeserializer getDeserializer() {
|
||||
return deserializer;
|
||||
public boolean getReadFriends() {
|
||||
return bindingConf.readFriends;
|
||||
}
|
||||
|
||||
public boolean 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() {
|
||||
return (String) getThing().getConfiguration().get("id");
|
||||
return (String) getThing().getConfiguration().get(NAThingConfiguration.ID);
|
||||
}
|
||||
|
||||
default Stream<Channel> getActiveChannels() {
|
||||
|
|
|
@ -17,19 +17,18 @@ import java.util.Optional;
|
|||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
|
||||
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
|
||||
* subject to receive event notifications. This class registers to webhookservlet so
|
||||
* it can be notified when an event arrives.
|
||||
* {@link EventCapability} is the base class for handlers subject to receive event notifications.
|
||||
* This class registers to NetatmoServletService so it can be notified when an event arrives.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class EventCapability extends Capability {
|
||||
private Optional<NetatmoServlet> servlet = Optional.empty();
|
||||
private Optional<WebhookServlet> webhook = Optional.empty();
|
||||
|
||||
public EventCapability(CommonInterface handler) {
|
||||
super(handler);
|
||||
|
@ -39,13 +38,13 @@ public class EventCapability extends Capability {
|
|||
public void initialize() {
|
||||
ApiBridgeHandler accountHandler = handler.getAccountHandler();
|
||||
if (accountHandler != null) {
|
||||
servlet = accountHandler.getServlet();
|
||||
servlet.ifPresent(s -> s.registerDataListener(handler.getId(), this));
|
||||
webhook = accountHandler.getWebHookServlet();
|
||||
webhook.ifPresent(servlet -> servlet.registerDataListener(handler.getId(), this));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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.Nullable;
|
||||
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.binding.ThingTypeProvider;
|
||||
import org.openhab.core.thing.i18n.ThingTypeI18nLocalizationService;
|
||||
|
@ -73,7 +74,8 @@ public class NetatmoThingTypeProvider implements ThingTypeProvider {
|
|||
ModuleType moduleType = ModuleType.from(thingTypeUID);
|
||||
|
||||
ThingTypeBuilder thingTypeBuilder = ThingTypeBuilder.instance(thingTypeUID, thingTypeUID.toString())
|
||||
.withRepresentationProperty(EQUIPMENT_ID).withExtensibleChannelTypeIds(moduleType.extensions)
|
||||
.withRepresentationProperty(NAThingConfiguration.ID)
|
||||
.withExtensibleChannelTypeIds(moduleType.extensions)
|
||||
.withChannelGroupDefinitions(getGroupDefinitions(moduleType))
|
||||
.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>
|
||||
</parameter>
|
||||
|
||||
<parameter name="username" type="text" required="true">
|
||||
<label>Username</label>
|
||||
<description>Your Netatmo API username (email).</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="password" type="text" required="true">
|
||||
<label>Password</label>
|
||||
<description>Your Netatmo API password.</description>
|
||||
<parameter name="refreshToken" type="text">
|
||||
<label>Refresh Token</label>
|
||||
<description>Refresh token provided by the oAuth2 authentication process.</description>
|
||||
<context>password</context>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="webHookUrl" type="text" required="false">
|
||||
<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 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-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-no-password = Cannot connect to Netatmo bridge as no password is available in the configuration
|
||||
conf-error-grant-needed = Configuration incomplete, please grant the binding to Netatmo Connect.
|
||||
status-bridge-offline = Bridge is not connected to Netatmo API
|
||||
device-not-connected = Thing is not reachable
|
||||
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