[netatmo] Switch to Code Granting process (#12726)

Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital
2022-05-20 12:53:53 +02:00
committed by GitHub
parent c12ed4bc8e
commit 9632a0a870
22 changed files with 715 additions and 333 deletions

View File

@@ -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";

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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() {

View File

@@ -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()));
}
}

View File

@@ -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());

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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">

View File

@@ -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

View File

@@ -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>