[netatmo] Avoid endless loop when Security claims event history (#17484)

* Avoid looping in events

Signed-off-by: Gaël L'hopital <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital 2024-10-05 21:44:07 +02:00 committed by GitHub
parent 668c20b525
commit 6a6c7f6cc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 47 additions and 54 deletions

View File

@ -15,7 +15,6 @@ package org.openhab.binding.netatmo.internal.api;
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*; import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
import static org.openhab.core.auth.oauth2client.internal.Keyword.*; import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -37,8 +36,8 @@ import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
*/ */
@NonNullByDefault @NonNullByDefault
public class AuthenticationApi extends RestManager { public class AuthenticationApi extends RestManager {
public static final URI TOKEN_URI = getApiBaseBuilder(PATH_OAUTH, SUB_PATH_TOKEN).build(); public static final String TOKEN_URI = getApiBaseBuilder(PATH_OAUTH, SUB_PATH_TOKEN).build().toString();
public static final URI AUTH_URI = getApiBaseBuilder(PATH_OAUTH, SUB_PATH_AUTHORIZE).build(); public static final String AUTH_URI = getApiBaseBuilder(PATH_OAUTH, SUB_PATH_AUTHORIZE).build().toString();
private List<Scope> grantedScope = List.of(); private List<Scope> grantedScope = List.of();
private @Nullable String authorization; private @Nullable String authorization;
@ -47,16 +46,9 @@ public class AuthenticationApi extends RestManager {
super(bridge, FeatureArea.NONE); super(bridge, FeatureArea.NONE);
} }
public void setAccessToken(@Nullable String accessToken) { public void setAccessToken(@Nullable String accessToken, String scope) {
if (accessToken != null) { authorization = accessToken != null ? "Bearer " + accessToken : null;
authorization = "Bearer " + accessToken; grantedScope = Stream.of(scope.toUpperCase().split(" ")).map(Scope::valueOf).toList();
} else {
authorization = null;
}
}
public void setScope(String scope) {
grantedScope = Stream.of(scope.split(" ")).map(s -> Scope.valueOf(s.toUpperCase())).toList();
} }
public void dispose() { public void dispose() {

View File

@ -18,6 +18,8 @@ import java.net.URI;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
@ -70,12 +72,9 @@ public class SecurityApi extends RestManager {
private List<HomeEvent> getEvents(@Nullable Object... params) throws NetatmoException { private List<HomeEvent> getEvents(@Nullable Object... params) throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_GET_EVENTS, params); UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_GET_EVENTS, params);
BodyResponse<Home> body = get(uriBuilder, NAEventsDataResponse.class).getBody(); if (get(uriBuilder, NAEventsDataResponse.class).getBody() instanceof BodyResponse<Home> body
if (body != null) { && body.getElement() instanceof Home home) {
Home home = body.getElement(); return home.getEvents();
if (home != null) {
return home.getEvents();
}
} }
throw new NetatmoException("home should not be null"); throw new NetatmoException("home should not be null");
} }
@ -84,17 +83,20 @@ public class SecurityApi extends RestManager {
List<HomeEvent> events = getEvents(PARAM_HOME_ID, homeId); List<HomeEvent> events = getEvents(PARAM_HOME_ID, homeId);
// we have to rewind to the latest event just after freshestEventTime // we have to rewind to the latest event just after freshestEventTime
if (events.size() > 0) { if (!events.isEmpty()) {
String oldestId = "";
HomeEvent oldestRetrieved = events.get(events.size() - 1); HomeEvent oldestRetrieved = events.get(events.size() - 1);
while (oldestRetrieved.getTime().isAfter(freshestEventTime)) { while (oldestRetrieved.getTime().isAfter(freshestEventTime) && !oldestId.equals(oldestRetrieved.getId())) {
events.addAll(getEvents(PARAM_HOME_ID, homeId, PARAM_EVENT_ID, oldestRetrieved.getId())); oldestId = oldestRetrieved.getId();
events.addAll(getEvents(PARAM_HOME_ID, homeId, PARAM_EVENT_ID, oldestId, PARAM_SIZE, 300));
oldestRetrieved = events.get(events.size() - 1); oldestRetrieved = events.get(events.size() - 1);
} }
} }
// Remove unneeded events being before freshestEventTime // Remove potential duplicates then unneeded events being before freshestEventTime
return events.stream().filter(event -> event.getTime().isAfter(freshestEventTime)) return events.stream().filter(event -> event.getTime().isAfter(freshestEventTime))
.sorted(Comparator.comparing(HomeEvent::getTime).reversed()).toList(); .collect(Collectors.toConcurrentMap(HomeEvent::getId, Function.identity(), (p, q) -> p)).values()
.stream().sorted(Comparator.comparing(HomeEvent::getTime).reversed()).toList();
} }
public List<HomeEvent> getPersonEvents(String homeId, String personId) throws NetatmoException { public List<HomeEvent> getPersonEvents(String homeId, String personId) throws NetatmoException {

View File

@ -149,6 +149,7 @@ public class NetatmoConstants {
public static final String PARAM_EVENT_ID = "event_id"; public static final String PARAM_EVENT_ID = "event_id";
public static final String PARAM_SCHEDULE_ID = "schedule_id"; public static final String PARAM_SCHEDULE_ID = "schedule_id";
public static final String PARAM_OFFSET = "offset"; public static final String PARAM_OFFSET = "offset";
public static final String PARAM_SIZE = "size";
public static final String PARAM_GATEWAY_TYPE = "gateway_types"; public static final String PARAM_GATEWAY_TYPE = "gateway_types";
public static final String PARAM_MODE = "mode"; public static final String PARAM_MODE = "mode";
public static final String PARAM_URL = "url"; public static final String PARAM_URL = "url";

View File

@ -21,7 +21,8 @@ import java.io.InputStream;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Collection; import java.util.Collection;
import java.util.Deque; import java.util.Deque;
@ -35,6 +36,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@ -80,7 +82,6 @@ import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
@ -106,7 +107,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class); private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
private final AuthenticationApi connectApi = new AuthenticationApi(this); private final AuthenticationApi connectApi = new AuthenticationApi(this);
private final Map<Class<? extends RestManager>, RestManager> managers = new HashMap<>(); private final Map<Class<? extends RestManager>, RestManager> managers = new HashMap<>();
private final Deque<LocalDateTime> requestsTimestamps = new ArrayDeque<>(200); private final Deque<Instant> requestsTimestamps = new ArrayDeque<>(200);
private final BindingConfiguration bindingConf; private final BindingConfiguration bindingConf;
private final HttpClient httpClient; private final HttpClient httpClient;
private final OAuthFactory oAuthFactory; private final OAuthFactory oAuthFactory;
@ -150,9 +151,9 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
} }
oAuthClientService = oAuthFactory oAuthClientService = oAuthFactory
.createOAuthClientService(this.getThing().getUID().getAsString(), .createOAuthClientService(this.getThing().getUID().getAsString(), AuthenticationApi.TOKEN_URI,
AuthenticationApi.TOKEN_URI.toString(), AuthenticationApi.AUTH_URI.toString(), AuthenticationApi.AUTH_URI, configuration.clientId, configuration.clientSecret,
configuration.clientId, configuration.clientSecret, FeatureArea.ALL_SCOPES, false) FeatureArea.ALL_SCOPES, false)
.withGsonBuilder(new GsonBuilder().registerTypeAdapter(AccessTokenResponse.class, .withGsonBuilder(new GsonBuilder().registerTypeAdapter(AccessTokenResponse.class,
new AccessTokenResponseDeserializer())); new AccessTokenResponseDeserializer()));
@ -166,7 +167,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
return; return;
} }
logger.debug("Connecting to Netatmo API."); logger.debug("Connected to Netatmo API.");
ApiHandlerConfiguration configuration = getConfiguration(); ApiHandlerConfiguration configuration = getConfiguration();
if (!configuration.webHookUrl.isBlank()) { if (!configuration.webHookUrl.isBlank()) {
@ -181,10 +182,6 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
} }
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
getThing().getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler)
.filter(CommonInterface.class::isInstance).map(CommonInterface.class::cast)
.forEach(CommonInterface::expireData);
} }
private boolean authenticate(@Nullable String code, @Nullable String redirectUri) { private boolean authenticate(@Nullable String code, @Nullable String redirectUri) {
@ -221,9 +218,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
return false; return false;
} }
connectApi.setAccessToken(accessTokenResponse.getAccessToken()); connectApi.setAccessToken(accessTokenResponse.getAccessToken(), accessTokenResponse.getScope());
connectApi.setScope(accessTokenResponse.getScope());
return true; return true;
} }
@ -233,6 +228,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
grantServlet = Optional.of(servlet); grantServlet = Optional.of(servlet);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
ConfigurationLevel.REFRESH_TOKEN_NEEDED.message.formatted(servlet.getPath())); ConfigurationLevel.REFRESH_TOKEN_NEEDED.message.formatted(servlet.getPath()));
connectApi.dispose();
} }
public ApiHandlerConfiguration getConfiguration() { public ApiHandlerConfiguration getConfiguration() {
@ -317,20 +313,21 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
InputStream stream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8)); InputStream stream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
try (InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(stream)) { try (InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(stream)) {
request.content(inputStreamContentProvider, contentType); request.content(inputStreamContentProvider, contentType);
request.header(HttpHeader.ACCEPT, "application/json"); request.header(HttpHeader.ACCEPT, MediaType.APPLICATION_JSON);
} }
logger.trace(" -with payload: {} ", payload); logger.trace(" -with payload: {} ", payload);
} }
if (isLinked(requestCountChannelUID)) { if (isLinked(requestCountChannelUID)) {
LocalDateTime now = LocalDateTime.now(); Instant now = Instant.now();
LocalDateTime oneHourAgo = now.minusHours(1);
requestsTimestamps.addLast(now); requestsTimestamps.addLast(now);
Instant oneHourAgo = now.minus(1, ChronoUnit.HOURS);
while (requestsTimestamps.getFirst().isBefore(oneHourAgo)) { while (requestsTimestamps.getFirst().isBefore(oneHourAgo)) {
requestsTimestamps.removeFirst(); requestsTimestamps.removeFirst();
} }
updateState(requestCountChannelUID, new DecimalType(requestsTimestamps.size())); updateState(requestCountChannelUID, new DecimalType(requestsTimestamps.size()));
} }
logger.trace(" -with headers: {} ", logger.trace(" -with headers: {} ",
String.join(", ", request.getHeaders().stream().map(HttpField::toString).toList())); String.join(", ", request.getHeaders().stream().map(HttpField::toString).toList()));
ContentResponse response = request.send(); ContentResponse response = request.send();
@ -355,6 +352,8 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
if (e.getStatusCode() == ServiceError.MAXIMUM_USAGE_REACHED) { if (e.getStatusCode() == ServiceError.MAXIMUM_USAGE_REACHED) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/maximum-usage-reached"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/maximum-usage-reached");
prepareReconnection(null, null); prepareReconnection(null, null);
} else if (e.getStatusCode() == ServiceError.INVALID_TOKEN_MISSING) {
startAuthorizationFlow();
} }
throw e; throw e;
} catch (InterruptedException e) { } catch (InterruptedException e) {

View File

@ -68,7 +68,6 @@ class SecurityCapability extends RestCapability<SecurityApi> {
@Override @Override
public void initialize() { public void initialize() {
super.initialize(); super.initialize();
freshestEventTime = ZDT_REFERENCE;
securityId = handler.getThingConfigAs(HomeConfiguration.class).getIdForArea(FeatureArea.SECURITY); securityId = handler.getThingConfigAs(HomeConfiguration.class).getIdForArea(FeatureArea.SECURITY);
} }
@ -124,28 +123,28 @@ class SecurityCapability extends RestCapability<SecurityApi> {
protected List<NAObject> updateReadings(SecurityApi api) { protected List<NAObject> updateReadings(SecurityApi api) {
List<NAObject> result = new ArrayList<>(); List<NAObject> result = new ArrayList<>();
try { try {
for (HomeEvent event : api.getHomeEvents(securityId, freshestEventTime)) { api.getHomeEvents(securityId, freshestEventTime).stream().forEach(event -> {
HomeEvent previousEvent = eventBuffer.get(event.getCameraId()); bufferIfNewer(event.getCameraId(), event);
if (previousEvent == null || previousEvent.getTime().isBefore(event.getTime())) { if (event.getPersonId() instanceof String personId) {
eventBuffer.put(event.getCameraId(), event); bufferIfNewer(personId, event);
}
String personId = event.getPersonId();
if (personId != null) {
previousEvent = eventBuffer.get(personId);
if (previousEvent == null || previousEvent.getTime().isBefore(event.getTime())) {
eventBuffer.put(personId, event);
}
} }
if (event.getTime().isAfter(freshestEventTime)) { if (event.getTime().isAfter(freshestEventTime)) {
freshestEventTime = event.getTime(); freshestEventTime = event.getTime();
} }
} });
} catch (NetatmoException e) { } catch (NetatmoException e) {
logger.warn("Error retrieving last events for home '{}' : {}", securityId, e.getMessage()); logger.warn("Error retrieving last events for home '{}' : {}", securityId, e.getMessage());
} }
return result; return result;
} }
private void bufferIfNewer(String id, HomeEvent event) {
HomeEvent previousEvent = eventBuffer.get(id);
if (previousEvent == null || previousEvent.getTime().isBefore(event.getTime())) {
eventBuffer.put(id, event);
}
}
public NAObjectMap<HomeDataPerson> getPersons() { public NAObjectMap<HomeDataPerson> getPersons() {
return persons; return persons;
} }