Skip to content

Commit

Permalink
Simplify connection handling, PR review changes.
Browse files Browse the repository at this point in the history
Signed-off-by: Dan Cunningham <[email protected]>
  • Loading branch information
digitaldan committed Dec 26, 2020
1 parent 823a410 commit e90a131
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 147 deletions.
16 changes: 8 additions & 8 deletions bundles/org.openhab.binding.generacmobilelink/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ All channels are read-only.
| yellowLight | Switch | Yellow light state |
| redLight | Switch | Red light state (typically off mode) |
| blueLight | Switch | Blue light state (typically running mode) |
| statusDate | String | Status date |
| status | String | Status |
| statusDate | DateTime | Status date (start of day) |
| status | String | General status |
| currentAlarmDescription | String | Current alarm description |
| runHours | Number:Time | Run hours |
| exerciseHours | Number:Time | Exercise hours |
| fuelType | Number | Fuel Type |
| fuelLevel | Number:Dimensionless | Fuel Level |
| batteryVoltage | String | Battery Voltage Status |
| serviceStatus | Switch | Service Status |
| runHours | Number:Time | Number of run hours |
| exerciseHours | Number:Time | Number of exercise hours |
| fuelType | Number | Fuel type |
| fuelLevel | Number:Dimensionless | Fuel level |
| batteryVoltage | String | Battery voltage status |
| serviceStatus | Switch | Service status |


## Full Example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,33 @@

import static org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants.THING_TYPE_GENERATOR;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryListener;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.config.discovery.ScanListener;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;

/**
* The {@link GeneracMobileLinkDiscoveryService} is responsible for discovering generator things
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class GeneracMobileLinkDiscoveryService implements DiscoveryService {
public class GeneracMobileLinkDiscoveryService extends AbstractDiscoveryService {
private static final Set<ThingTypeUID> SUPPORTED_DISCOVERY_THING_TYPES_UIDS = Set.of(THING_TYPE_GENERATOR);
private final Map<ThingUID, DiscoveryResult> cachedResults = new HashMap<>();
private final Set<DiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>();

public void generatorDiscovered(DiscoveryResult result) {
for (DiscoveryListener discoveryListener : discoveryListeners) {
discoveryListener.thingDiscovered(this, result);
}
synchronized (cachedResults) {
cachedResults.put(result.getThingUID(), result);
}
public GeneracMobileLinkDiscoveryService() {
super(SUPPORTED_DISCOVERY_THING_TYPES_UIDS, 0);
}

@Override
public Collection<ThingTypeUID> getSupportedThingTypes() {
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_DISCOVERY_THING_TYPES_UIDS;
}

@Override
public int getScanTimeout() {
return 0;
public void startScan() {
}

@Override
Expand All @@ -65,31 +49,7 @@ public boolean isBackgroundDiscoveryEnabled() {
}

@Override
public void startScan(@Nullable ScanListener listener) {
if (listener != null) {
listener.onFinished();
}
}

@Override
public void abortScan() {
}

@Override
public void addDiscoveryListener(@Nullable DiscoveryListener listener) {
if (listener == null) {
return;
}
synchronized (cachedResults) {
for (DiscoveryResult cachedResult : cachedResults.values()) {
listener.thingDiscovered(this, cachedResult);
}
}
discoveryListeners.add(listener);
}

@Override
public void removeDiscoveryListener(@Nullable DiscoveryListener listener) {
discoveryListeners.remove(listener);
public void thingDiscovered(DiscoveryResult result) {
super.thingDiscovered(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2020 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.generacmobilelink.internal.dto;

/**
* {@link ErrorResponseDTO} object from the MobileLink API
*
* @author Dan Cunningham - Initial contribution
*/
public class ErrorResponseDTO {
public Integer errorCode;
public String errorMessage;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

import static org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants.*;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
Expand Down Expand Up @@ -50,7 +50,7 @@
public class GeneracMobileLinkHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT,
THING_TYPE_GENERATOR);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new ConcurrentHashMap<>();
private final HttpClient httpClient;

@Activate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.StringContentProvider;
Expand All @@ -31,6 +31,7 @@
import org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants;
import org.openhab.binding.generacmobilelink.internal.config.GeneracMobileLinkAccountConfiguration;
import org.openhab.binding.generacmobilelink.internal.discovery.GeneracMobileLinkDiscoveryService;
import org.openhab.binding.generacmobilelink.internal.dto.ErrorResponseDTO;
import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusDTO;
import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusResponseDTO;
import org.openhab.binding.generacmobilelink.internal.dto.LoginRequestDTO;
Expand Down Expand Up @@ -124,103 +125,106 @@ private void restartPoll() {
}

private void poll() {
// if our token is null we need to login
if (authToken == null) {
logger.debug("login");
logger.debug("Attempting Login");
login();
}

// if we now have a token, get our data
if (authToken != null) {
getStatuses(true);
}
getStatuses(true);
}

private synchronized void login() {
GeneracMobileLinkAccountConfiguration config = getConfigAs(GeneracMobileLinkAccountConfiguration.class);
refreshIntervalSeconds = config.refreshInterval;
try {
// use asynchronous Jetty for login as the API service will response with a 401 error when credentials are
// wrong, but not a WWW-Authenticate header which causes synchronous Jetty to throw a generic execution
// exception which prevents us from knowing the response code
GeneracMobileLinkAccountConfiguration config = getConfigAs(GeneracMobileLinkAccountConfiguration.class);
refreshIntervalSeconds = config.refreshInterval;
final CompletableFuture<AsyncResult> futureResult = new CompletableFuture<>();
httpClient.newRequest(BASE_URL + "/Users/login").method(HttpMethod.POST)
.content(
new StringContentProvider(
gson.toJson(new LoginRequestDTO(SHARED_KEY, config.username, config.password))),
"application/json")
.timeout(10, TimeUnit.SECONDS).send(new BufferingResponseListener() {
@NonNullByDefault({})
@Override
public void onComplete(Result result) {
futureResult
.complete(new AsyncResult(getContentAsString(), result.getResponse().getStatus()));
}
});
AsyncResult result = futureResult.get();
switch (result.responseCode) {
case HttpStatus.OK_200:
LoginResponseDTO loginResponse = gson.fromJson(result.content, LoginResponseDTO.class);
if (loginResponse != null) {
authToken = loginResponse.authToken;
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Invalid Response Body");
}
break;
case HttpStatus.UNAUTHORIZED_401:
// the server responds with a 500 error in some cases when credentials are not correct
case HttpStatus.INTERNAL_SERVER_ERROR_500:
// do not continue to poll with bad credentials since this requires user intervention
HTTPResult result = sendRequest(BASE_URL + "/Users/login", HttpMethod.POST, null,
new StringContentProvider(
gson.toJson(new LoginRequestDTO(SHARED_KEY, config.username, config.password))),
"application/json");
if (result.responseCode == HttpStatus.OK_200) {
LoginResponseDTO loginResponse = gson.fromJson(result.content, LoginResponseDTO.class);
if (loginResponse != null) {
authToken = loginResponse.authToken;
updateStatus(ThingStatus.ONLINE);
}
} else {
handleErrorResponse(result);
if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR) {
// bad credentials, stop trying to login
stopPoll();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Unauthorized: " + result.content);
break;
default:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Invalid Response Code " + result.responseCode);
}
}

} catch (ExecutionException | InterruptedException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
} catch (InterruptedException e) {
}
}

private void getStatuses(boolean retry) {
if (authToken == null) {
return;
}
try {
ContentResponse contentResponse = httpClient.newRequest(BASE_URL + "/Generator/GeneratorStatus")
.method(HttpMethod.GET).timeout(10, TimeUnit.SECONDS).header("AuthToken", authToken).send();
int httpStatus = contentResponse.getStatus();
String content = contentResponse.getContentAsString();
logger.trace("GeneratorStatusResponse - status: {} content: {}", httpStatus, content);
switch (httpStatus) {
case HttpStatus.OK_200:
generators = gson.fromJson(content, GeneratorStatusResponseDTO.class);
updateGeneratorThings();
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
break;
case HttpStatus.UNAUTHORIZED_401:
authToken = null;
restartPoll();
break;
default:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Invalid Return Code " + httpStatus);
}
} catch (ExecutionException | InterruptedException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
} catch (TimeoutException e) {
// the API seems to time out on this call frequently, although recovers after trying again
if (retry) {
logger.debug("Timeout occured, Retying status request");
getStatuses(false);
HTTPResult result = sendRequest(BASE_URL + "/Generator/GeneratorStatus", HttpMethod.GET, authToken, null,
null);
if (result.responseCode == HttpStatus.OK_200) {
generators = gson.fromJson(result.content, GeneratorStatusResponseDTO.class);
updateGeneratorThings();
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
} else {
logger.debug("Timeout occured, waiting for next poll cycle");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
if (retry) {
logger.debug("Retrying status request");
getStatuses(false);
} else {
handleErrorResponse(result);
}
}
} catch (InterruptedException e) {
}
}

private HTTPResult sendRequest(String url, HttpMethod method, @Nullable String token,
@Nullable ContentProvider content, @Nullable String contentType) throws InterruptedException {
try {
Request request = httpClient.newRequest(url).method(method).timeout(10, TimeUnit.SECONDS);
if (token != null) {
request = request.header("AuthToken", token);
}
if (content != null & contentType != null) {
request = request.content(content, contentType);
}
logger.trace("Sending {} to {}", request.getMethod(), request.getURI());
final CompletableFuture<HTTPResult> futureResult = new CompletableFuture<>();
request.send(new BufferingResponseListener() {
@NonNullByDefault({})
@Override
public void onComplete(Result result) {
futureResult.complete(new HTTPResult(result.getResponse().getStatus(), getContentAsString()));
}
});
HTTPResult result = futureResult.get();
logger.trace("Response - status: {} content: {}", result.responseCode, result.content);
return result;
} catch (ExecutionException e) {
logger.debug("request failed", e);
return new HTTPResult(0, e.getMessage());
}
}

private void handleErrorResponse(HTTPResult result) {
switch (result.responseCode) {
case HttpStatus.UNAUTHORIZED_401:
// the server responds with a 500 error in some cases when credentials are not correct
case HttpStatus.INTERNAL_SERVER_ERROR_500:
// server returned a valid error response
ErrorResponseDTO error = gson.fromJson(result.content, ErrorResponseDTO.class);
if (error != null && error.errorCode > 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Unauthorized: " + result.content);
authToken = null;
break;
}
default:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, result.content);
}
}

Expand Down Expand Up @@ -249,16 +253,16 @@ private void generatorDiscovered(GeneratorStatusDTO generator) {
.withLabel("MobileLink Generator " + generator.generatorName)
.withProperty("generatorId", String.valueOf(generator.gensetID))
.withRepresentationProperty("generatorId").withBridge(getThing().getUID()).build();
discoveryService.generatorDiscovered(result);
discoveryService.thingDiscovered(result);
}

public static class AsyncResult {
public static class HTTPResult {
public @Nullable String content;
public final int responseCode;

public AsyncResult(@Nullable String content, int responseCode) {
this.content = content;
public HTTPResult(int responseCode, @Nullable String content) {
this.responseCode = responseCode;
this.content = content;
}
}
}
Loading

0 comments on commit e90a131

Please sign in to comment.