[senseenergy] Address reconnect issues on failure (#18463)
* Address reconnect issues on failure Signed-off-by: Jeff James <jeff@james-online.com>pull/18636/head
parent
d86fea3104
commit
4b175571be
|
@ -18,8 +18,6 @@ import java.time.Instant;
|
|||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
import javax.measure.quantity.Energy;
|
||||
|
@ -105,7 +103,7 @@ public class SenseEnergyMonitorActions implements ThingActions {
|
|||
SenseEnergyApiGetTrends trends;
|
||||
try {
|
||||
trends = localDeviceHandler.getApi().getTrendData(localDeviceHandler.getId(), trendScale, localDateTime);
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) {
|
||||
} catch (SenseEnergyApiException e) {
|
||||
logger.warn("queryEnergyTrends function failed - {}", e.getMessage());
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.eclipse.jetty.client.util.FormContentProvider;
|
|||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyApiException.SEVERITY;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiAuthenticate;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiDevice;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiGetTrends;
|
||||
|
@ -60,7 +61,6 @@ import com.google.gson.JsonSyntaxException;
|
|||
* implementation here: https://github.com/scottbonline/sense
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyApi {
|
||||
|
@ -119,17 +119,8 @@ public class SenseEnergyApi {
|
|||
* @param password
|
||||
*
|
||||
* @return a set of IDs for all the monitors associated with this account
|
||||
*
|
||||
* @throws SenseEnergyApiException on authentication error
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*/
|
||||
public Set<Long> initialize(String email, String password)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
public Set<Long> initialize(String email, String password) throws SenseEnergyApiException {
|
||||
Fields fields = new Fields();
|
||||
fields.put("email", email);
|
||||
fields.put("password", password);
|
||||
|
@ -143,7 +134,7 @@ public class SenseEnergyApi {
|
|||
SenseEnergyApiAuthenticate.class);
|
||||
|
||||
if (data == null) {
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", false);
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", SenseEnergyApiException.SEVERITY.FATAL);
|
||||
}
|
||||
|
||||
accessToken = data.accessToken;
|
||||
|
@ -157,17 +148,8 @@ public class SenseEnergyApi {
|
|||
|
||||
/*
|
||||
* renew authentication credentials. Timeout of credentials is ~24 hours.
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*
|
||||
* @throws SenseEnergyApiException
|
||||
*/
|
||||
public void refreshToken()
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
public void refreshToken() throws SenseEnergyApiException {
|
||||
Fields fields = new Fields();
|
||||
fields.add("user_id", Long.toString(this.userID));
|
||||
fields.add("refresh_token", this.refreshToken);
|
||||
|
@ -181,7 +163,7 @@ public class SenseEnergyApi {
|
|||
SenseEnergyApiRefreshToken.class);
|
||||
|
||||
if (data == null) {
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", false);
|
||||
throw new SenseEnergyApiException("text/api.response-invalid", SenseEnergyApiException.SEVERITY.TRANSIENT);
|
||||
}
|
||||
|
||||
logger.debug("Successful refreshToken {}", data.accessToken);
|
||||
|
@ -192,7 +174,7 @@ public class SenseEnergyApi {
|
|||
tokenExpiresAt = data.expires.minus(1, ChronoUnit.HOURS); // refresh an hour before token expires
|
||||
}
|
||||
|
||||
public void logout() throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
public void logout() throws SenseEnergyApiException {
|
||||
Request request = httpClient.newRequest(APIURL_LOGOUT).method(HttpMethod.GET);
|
||||
|
||||
sendRequest(request);
|
||||
|
@ -204,17 +186,8 @@ public class SenseEnergyApi {
|
|||
* @param id of the monitor
|
||||
*
|
||||
* @return dto structure containing monitor info
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*
|
||||
* @throws SenseEnergyApiException
|
||||
*/
|
||||
public SenseEnergyApiMonitor getMonitorOverview(long id)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
public SenseEnergyApiMonitor getMonitorOverview(long id) throws SenseEnergyApiException {
|
||||
String url = String.format(APIURL_MONITOR_OVERVIEW, id);
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
|
@ -222,17 +195,11 @@ public class SenseEnergyApi {
|
|||
|
||||
try {
|
||||
JsonObject jsonResponse = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
SenseEnergyApiMonitor monitor = gson.fromJson(
|
||||
jsonResponse.getAsJsonObject("monitor_overview").getAsJsonObject("monitor"),
|
||||
SenseEnergyApiMonitor.class);
|
||||
|
||||
if (monitor == null) {
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", false);
|
||||
}
|
||||
|
||||
return monitor;
|
||||
return apiRequireNonNull(
|
||||
gson.fromJson(jsonResponse.getAsJsonObject("monitor_overview").getAsJsonObject("monitor"),
|
||||
SenseEnergyApiMonitor.class));
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", false);
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", SenseEnergyApiException.SEVERITY.TRANSIENT);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,32 +209,18 @@ public class SenseEnergyApi {
|
|||
* @param id - id of monitor
|
||||
*
|
||||
* @return dto structure containing monitor status
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*
|
||||
* @throws SenseEnergyApiException
|
||||
*/
|
||||
@Nullable
|
||||
public SenseEnergyApiMonitorStatus getMonitorStatus(long id)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
public SenseEnergyApiMonitorStatus getMonitorStatus(long id) throws SenseEnergyApiException {
|
||||
String url = String.format(APIURL_MONITOR_STATUS, id);
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
ContentResponse response = sendRequest(request);
|
||||
|
||||
final SenseEnergyApiMonitorStatus data = gson.fromJson(response.getContentAsString(),
|
||||
SenseEnergyApiMonitorStatus.class);
|
||||
|
||||
return data;
|
||||
return apiRequireNonNull(gson.fromJson(response.getContentAsString(), SenseEnergyApiMonitorStatus.class));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SenseEnergyApiGetTrends getTrendData(long id, TrendScale trendScale)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
public SenseEnergyApiGetTrends getTrendData(long id, TrendScale trendScale) throws SenseEnergyApiException {
|
||||
return getTrendData(id, trendScale, Instant.now());
|
||||
}
|
||||
|
||||
|
@ -281,27 +234,16 @@ public class SenseEnergyApi {
|
|||
* @param datetime a datetime within the scale of which to receive data. Does not need to be the start or end .
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*
|
||||
* @throws SenseEnergyApiException
|
||||
*/
|
||||
@Nullable
|
||||
public SenseEnergyApiGetTrends getTrendData(long id, TrendScale trendScale, Instant datetime)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
throws SenseEnergyApiException {
|
||||
String url = String.format(APIURL_GET_TRENDS, id, trendScale.toString(), datetime.toString());
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
ContentResponse response = sendRequest(request);
|
||||
|
||||
final SenseEnergyApiGetTrends data = gson.fromJson(response.getContentAsString(),
|
||||
SenseEnergyApiGetTrends.class);
|
||||
|
||||
return data;
|
||||
return gson.fromJson(response.getContentAsString(), SenseEnergyApiGetTrends.class);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -323,17 +265,8 @@ public class SenseEnergyApi {
|
|||
* @param id of the monitor device
|
||||
*
|
||||
* @return Map of discovered devices with the ID of the device as key and the dto object SenseEnergyApiDevice
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*
|
||||
* @throws SenseEnergyApiException
|
||||
*/
|
||||
public Map<String, SenseEnergyApiDevice> getDevices(long id)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
public Map<String, SenseEnergyApiDevice> getDevices(long id) throws SenseEnergyApiException {
|
||||
String url = String.format(APIURL_GET_DEVICES, id);
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
|
@ -341,9 +274,6 @@ public class SenseEnergyApi {
|
|||
|
||||
JsonArray jsonDevices = JsonParser.parseString(response.getContentAsString()).getAsJsonArray();
|
||||
|
||||
@SuppressWarnings("null") // prevent this warning on d.tags - [WARNING] Potential null pointer access: this
|
||||
// expression has
|
||||
// a '@Nullable' type
|
||||
Map<String, SenseEnergyApiDevice> mapDevices = StreamSupport.stream(jsonDevices.spliterator(), false) //
|
||||
.map(j -> jsonToSenseEnergyDevice(j)) //
|
||||
.filter(Objects::nonNull) //
|
||||
|
@ -362,28 +292,34 @@ public class SenseEnergyApi {
|
|||
return request;
|
||||
}
|
||||
|
||||
public void verifyToken()
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
public void verifyToken() throws SenseEnergyApiException {
|
||||
if (tokenExpiresAt.isBefore(Instant.now())) {
|
||||
refreshToken();
|
||||
}
|
||||
}
|
||||
|
||||
ContentResponse sendRequest(Request request)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
ContentResponse sendRequest(Request request) throws SenseEnergyApiException {
|
||||
return sendRequest(request, true);
|
||||
}
|
||||
|
||||
ContentResponse sendRequest(Request request, boolean verifyToken)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
ContentResponse sendRequest(Request request, boolean verifyToken) throws SenseEnergyApiException {
|
||||
if (verifyToken) {
|
||||
verifyToken();
|
||||
}
|
||||
|
||||
setHeaders(request);
|
||||
|
||||
logger.trace("REQUEST: {}", request.toString());
|
||||
ContentResponse response = request.send();
|
||||
ContentResponse response;
|
||||
try {
|
||||
logger.trace("REQUEST: {}", request.toString());
|
||||
response = request.send();
|
||||
} catch (InterruptedException e) {
|
||||
throw new SenseEnergyApiException("@text/api.connection-closed", SEVERITY.FATAL, e);
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
throw new SenseEnergyApiException("@text/api.connection-timeout", SEVERITY.TRANSIENT, e);
|
||||
} catch (Exception e) {
|
||||
throw new SenseEnergyApiException("@text/api.request-error", SenseEnergyApiException.SEVERITY.TRANSIENT, e);
|
||||
}
|
||||
logger.trace("RESPONSE: {}", response.getContentAsString());
|
||||
|
||||
switch (response.getStatus()) {
|
||||
|
@ -391,13 +327,24 @@ public class SenseEnergyApi {
|
|||
break;
|
||||
case 400: // API responses with 400 when user credentials are invalid
|
||||
case 401:
|
||||
throw new SenseEnergyApiException("@text/api.invalid-user-credentials", true);
|
||||
throw new SenseEnergyApiException("@text/api.invalid-user-credentials",
|
||||
SenseEnergyApiException.SEVERITY.CONFIG);
|
||||
case 429:
|
||||
throw new SenseEnergyApiException("@text/api.rate-limit-exceeded", false);
|
||||
throw new SenseEnergyApiException("@text/api.rate-limit-exceeded",
|
||||
SenseEnergyApiException.SEVERITY.TRANSIENT);
|
||||
default:
|
||||
throw new SenseEnergyApiException("Unexpected API error: " + response.getReason(), false);
|
||||
throw new SenseEnergyApiException("Unexpected API error: " + response.getReason(),
|
||||
SenseEnergyApiException.SEVERITY.TRANSIENT);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static <T> T apiRequireNonNull(@Nullable T obj) throws SenseEnergyApiException {
|
||||
if (obj == null) {
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", SenseEnergyApiException.SEVERITY.TRANSIENT);
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
package org.openhab.binding.senseenergy.internal.api;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyApiException} exception class for any api exception
|
||||
|
@ -22,20 +23,33 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||
@NonNullByDefault
|
||||
public class SenseEnergyApiException extends Exception {
|
||||
private static final long serialVersionUID = -7059398508028583720L;
|
||||
private final boolean configurationIssue;
|
||||
public final SEVERITY severity;
|
||||
@Nullable
|
||||
public final Exception e;
|
||||
|
||||
public SenseEnergyApiException(String message, boolean configurationIssue) {
|
||||
super(message);
|
||||
this.configurationIssue = configurationIssue;
|
||||
public static enum SEVERITY {
|
||||
CONFIG,
|
||||
TRANSIENT,
|
||||
DATA,
|
||||
FATAL
|
||||
}
|
||||
|
||||
public boolean isConfigurationIssue() {
|
||||
return configurationIssue;
|
||||
public SenseEnergyApiException(String message, SEVERITY severity) {
|
||||
super(message);
|
||||
this.severity = severity;
|
||||
this.e = null;
|
||||
}
|
||||
|
||||
public SenseEnergyApiException(String message, SEVERITY severity, Exception e) {
|
||||
super(message);
|
||||
this.severity = severity;
|
||||
this.e = e;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("SenseEnergyApiException{message='%s', configurationIssue=%b}", getMessage(),
|
||||
configurationIssue);
|
||||
Exception localE = e;
|
||||
return String.format("SenseEnergyApiException{message='%s', severity=%s}",
|
||||
(localE == null) ? getMessage() : localE.getMessage(), severity.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import java.io.IOException;
|
|||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
@ -78,17 +79,12 @@ public class SenseEnergyDatagram {
|
|||
}
|
||||
|
||||
public void stop() {
|
||||
connected = false;
|
||||
|
||||
try {
|
||||
DatagramSocket localSocket = datagramSocket;
|
||||
if (localSocket != null) {
|
||||
localSocket.close();
|
||||
datagramSocket = null;
|
||||
logger.debug("Closing datagram listener");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
logger.debug("closeConnection(): Error closing connection - {}", exception.getMessage());
|
||||
logger.debug("datagram stop");
|
||||
Thread localUdpThread = udpListener;
|
||||
if (localUdpThread != null) {
|
||||
connected = false;
|
||||
localUdpThread.interrupt();
|
||||
udpListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,7 +99,6 @@ public class SenseEnergyDatagram {
|
|||
}
|
||||
|
||||
private class UDPListener implements Runnable {
|
||||
|
||||
/*
|
||||
* Run method. Runs the MessageListener thread
|
||||
*/
|
||||
|
@ -121,41 +116,48 @@ public class SenseEnergyDatagram {
|
|||
|
||||
DatagramPacket packet = new DatagramPacket(new byte[BUFFERSIZE], BUFFERSIZE);
|
||||
|
||||
while (connected) {
|
||||
try {
|
||||
localSocket.receive(packet);
|
||||
} catch (IOException e) {
|
||||
logger.debug("Exception during packet read - {}", e.getMessage());
|
||||
try {
|
||||
while (connected && !localSocket.isClosed() && !Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
localSocket.receive(packet);
|
||||
} catch (ClosedByInterruptException e) {
|
||||
logger.debug("ClosedByInterruptExcepetion");
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
logger.debug("Exception during packet read - {}", e.getMessage());
|
||||
Thread.sleep(100); // allow CPU to breath
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// don't receive more than 1 request a second. Necessary to filter out receiving the same
|
||||
// broadcast request packet on multiple interfaces (i.e. wi-fi and wired) at the same time
|
||||
if (System.nanoTime() < nextPacketTime) {
|
||||
continue;
|
||||
}
|
||||
// don't receive more than 1 request a second. Necessary to filter out receiving the same
|
||||
// broadcast request packet on multiple interfaces (i.e. wi-fi and wired) at the same time
|
||||
if (System.nanoTime() < nextPacketTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonObject jsonResponse;
|
||||
String decryptedPacket = new String(TpLinkEncryption.decrypt(packet.getData(), packet.getLength()));
|
||||
try {
|
||||
jsonResponse = JsonParser.parseString(decryptedPacket).getAsJsonObject();
|
||||
} catch (JsonSyntaxException jsonSyntaxException) {
|
||||
logger.trace("Invalid JSON received");
|
||||
continue;
|
||||
}
|
||||
JsonObject jsonResponse;
|
||||
String decryptedPacket = new String(TpLinkEncryption.decrypt(packet.getData(), packet.getLength()));
|
||||
try {
|
||||
jsonResponse = JsonParser.parseString(decryptedPacket).getAsJsonObject();
|
||||
} catch (JsonSyntaxException jsonSyntaxException) {
|
||||
logger.trace("Invalid JSON received");
|
||||
continue;
|
||||
}
|
||||
|
||||
nextPacketTime = System.nanoTime() + 1000000000L;
|
||||
if (jsonResponse.has("system") && jsonResponse.has("emeter")) {
|
||||
SenseEnergyDatagramListener localPacketListener = packetListener;
|
||||
if (localPacketListener != null) {
|
||||
localPacketListener.requestReceived(packet.getSocketAddress());
|
||||
nextPacketTime = System.nanoTime() + 1000000000L;
|
||||
if (jsonResponse.has("system") && jsonResponse.has("emeter")) {
|
||||
SenseEnergyDatagramListener localPacketListener = packetListener;
|
||||
if (localPacketListener != null) {
|
||||
localPacketListener.requestReceived(packet.getSocketAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException | ClosedByInterruptException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
localSocket.close();
|
||||
datagramSocket = null;
|
||||
connected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,12 @@
|
|||
*/
|
||||
package org.openhab.binding.senseenergy.internal.api;
|
||||
|
||||
import static org.openhab.binding.senseenergy.internal.SenseEnergyBindingConstants.HEARTBEAT_MINUTES;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
@ -51,6 +54,10 @@ public class SenseEnergyWebSocket implements WebSocketListener {
|
|||
private boolean closing;
|
||||
private long monitorId;
|
||||
|
||||
private static int BACKOFF_TIME_START = 300;
|
||||
private static int BACKOFF_TIME_MAX = (int) Duration.ofMinutes(HEARTBEAT_MINUTES).toMillis();
|
||||
private int backOffTime = BACKOFF_TIME_START;
|
||||
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public boolean isClosing() {
|
||||
|
@ -62,7 +69,8 @@ public class SenseEnergyWebSocket implements WebSocketListener {
|
|||
this.client = client;
|
||||
}
|
||||
|
||||
public void start(long monitorId, String accessToken) throws Exception {
|
||||
public void start(long monitorId, String accessToken)
|
||||
throws InterruptedException, ExecutionException, IOException, URISyntaxException {
|
||||
logger.debug("Starting Sense Energy WebSocket for monitor ID: {}", monitorId);
|
||||
this.monitorId = monitorId;
|
||||
|
||||
|
@ -71,16 +79,26 @@ public class SenseEnergyWebSocket implements WebSocketListener {
|
|||
}
|
||||
|
||||
public void restart(String accessToken)
|
||||
throws InterruptedException, ExecutionException, IOException, URISyntaxException, Exception {
|
||||
throws InterruptedException, ExecutionException, IOException, URISyntaxException {
|
||||
logger.debug("Re-starting Sense Energy WebSocket");
|
||||
|
||||
stop();
|
||||
start(monitorId, accessToken);
|
||||
}
|
||||
|
||||
public void restartWithBackoff(String accessToken)
|
||||
throws InterruptedException, ExecutionException, IOException, URISyntaxException {
|
||||
logger.debug("Re-starting Sense Energy WebSocket - backoff {} ms", backOffTime);
|
||||
|
||||
stop();
|
||||
Thread.sleep(backOffTime);
|
||||
backOffTime = Math.min(backOffTime * 2, BACKOFF_TIME_MAX);
|
||||
start(monitorId, accessToken);
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
closing = true;
|
||||
logger.trace("Stopping Sense Energy WebSocket");
|
||||
logger.debug("Stopping Sense Energy WebSocket");
|
||||
|
||||
WebSocketSession localSession = session;
|
||||
if (localSession != null) {
|
||||
|
@ -115,6 +133,7 @@ public class SenseEnergyWebSocket implements WebSocketListener {
|
|||
public void onWebSocketConnect(@Nullable Session session) {
|
||||
closing = false;
|
||||
logger.debug("Connected to Sense Energy WebSocket");
|
||||
listener.onWebSocketConnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -139,6 +158,8 @@ public class SenseEnergyWebSocket implements WebSocketListener {
|
|||
return;
|
||||
}
|
||||
|
||||
logger.debug("onWebSocketText");
|
||||
|
||||
try {
|
||||
JsonObject jsonResponse = JsonParser.parseString(message).getAsJsonObject();
|
||||
String type = jsonResponse.get("type").getAsString();
|
||||
|
@ -150,6 +171,9 @@ public class SenseEnergyWebSocket implements WebSocketListener {
|
|||
if (update != null) {
|
||||
listener.onWebSocketRealtimeUpdate(update);
|
||||
}
|
||||
// Clear backoff time after a successful received packet to address issue of immediate Error/Close after
|
||||
// Connect
|
||||
backOffTime = BACKOFF_TIME_START;
|
||||
} else if ("error".equals(type)) {
|
||||
logger.warn("WebSocket error {}", jsonResponse.get("payload").toString());
|
||||
}
|
||||
|
|
|
@ -23,6 +23,11 @@ import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyWebSocketReal
|
|||
*/
|
||||
@NonNullByDefault
|
||||
public interface SenseEnergyWebSocketListener {
|
||||
/**
|
||||
* called when web socket connects
|
||||
*/
|
||||
void onWebSocketConnect();
|
||||
|
||||
/**
|
||||
* called when the web socket is closed
|
||||
*
|
||||
|
|
|
@ -18,10 +18,8 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
@ -92,7 +90,7 @@ public class SenseEnergyBridgeHandler extends BaseBridgeHandler {
|
|||
public void goOnline() {
|
||||
try {
|
||||
this.monitorIDs = api.initialize(config.email, config.password);
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) {
|
||||
} catch (SenseEnergyApiException e) {
|
||||
handleApiException(e);
|
||||
return;
|
||||
}
|
||||
|
@ -105,6 +103,7 @@ public class SenseEnergyBridgeHandler extends BaseBridgeHandler {
|
|||
}
|
||||
|
||||
private void heartbeat() {
|
||||
logger.trace("heartbeat");
|
||||
ThingStatus thingStatus = getThing().getStatus();
|
||||
|
||||
if (thingStatus == ThingStatus.OFFLINE
|
||||
|
@ -120,7 +119,7 @@ public class SenseEnergyBridgeHandler extends BaseBridgeHandler {
|
|||
// token is verified on each api call, called here in case no API calls are made in the alloted period
|
||||
try {
|
||||
getApi().verifyToken();
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) {
|
||||
} catch (SenseEnergyApiException e) {
|
||||
handleApiException(e);
|
||||
}
|
||||
|
||||
|
@ -133,17 +132,27 @@ public class SenseEnergyBridgeHandler extends BaseBridgeHandler {
|
|||
}
|
||||
|
||||
public void handleApiException(Exception e) {
|
||||
ThingStatusDetail statusDetail = ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR;
|
||||
|
||||
if (e instanceof SenseEnergyApiException apiException) {
|
||||
statusDetail = apiException.isConfigurationIssue() ? ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR
|
||||
: ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR;
|
||||
switch (apiException.severity) {
|
||||
case TRANSIENT:
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
|
||||
break;
|
||||
case CONFIG:
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR);
|
||||
break;
|
||||
case FATAL:
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.getMessage());
|
||||
break;
|
||||
case DATA:
|
||||
logger.warn("Data exception: {}", e.toString());
|
||||
break;
|
||||
default:
|
||||
logger.warn("SenseEnergyApiException: {}", e.toString());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logger.debug("Unhandled Exception", e);
|
||||
statusDetail = ThingStatusDetail.OFFLINE.NONE;
|
||||
logger.warn("Unhandled Exception", e);
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, statusDetail, e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -16,6 +16,7 @@ import static org.openhab.binding.senseenergy.internal.SenseEnergyBindingConstan
|
|||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -26,7 +27,6 @@ import java.util.Map;
|
|||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
@ -170,7 +170,7 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
|
|||
this.solarConfigured = apiMonitor.solarConfigured;
|
||||
apiMonitorStatus = getApi().getMonitorStatus(id);
|
||||
refreshDevices();
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) {
|
||||
} catch (SenseEnergyApiException e) {
|
||||
handleApiException(e);
|
||||
return;
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
|
|||
|
||||
try {
|
||||
webSocket.start(id, getApi().getAccessToken());
|
||||
} catch (Exception e) {
|
||||
} catch (InterruptedException | ExecutionException | IOException | URISyntaxException e) {
|
||||
handleApiException(e);
|
||||
return;
|
||||
}
|
||||
|
@ -221,7 +221,7 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
|
|||
logger.debug("heartbeat: webSocket not running");
|
||||
try {
|
||||
webSocket.restart(getApi().getAccessToken());
|
||||
} catch (Exception e) {
|
||||
} catch (InterruptedException | ExecutionException | IOException | URISyntaxException e) {
|
||||
handleApiException(e);
|
||||
}
|
||||
}
|
||||
|
@ -230,17 +230,27 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
|
|||
}
|
||||
|
||||
public void handleApiException(Exception e) {
|
||||
ThingStatusDetail statusDetail = ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR;
|
||||
|
||||
if (e instanceof SenseEnergyApiException apiException) {
|
||||
statusDetail = apiException.isConfigurationIssue() ? ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR
|
||||
: ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR;
|
||||
switch (apiException.severity) {
|
||||
case TRANSIENT:
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
|
||||
break;
|
||||
case CONFIG:
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR);
|
||||
break;
|
||||
case FATAL:
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.getMessage());
|
||||
break;
|
||||
case DATA:
|
||||
logger.warn("Data exception: {}", e.toString());
|
||||
break;
|
||||
default:
|
||||
logger.warn("SenseEnergyApiException: {}", e.toString());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logger.debug("Unhandled Exception", e);
|
||||
statusDetail = ThingStatusDetail.OFFLINE.NONE;
|
||||
logger.warn("Unhandled Exception", e);
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, statusDetail, e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -355,13 +365,14 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
|
|||
* Refreshes the list of devices by retrieving them from the API and then updating the map of DeviceTypes.
|
||||
*/
|
||||
private void refreshDevices() {
|
||||
logger.trace("refreshDevices");
|
||||
try {
|
||||
senseDevices = getApi().getDevices(id);
|
||||
|
||||
senseDevices.entrySet().stream() //
|
||||
.filter(e -> !senseDevicesType.containsKey(e.getKey())) //
|
||||
.forEach(e -> senseDevicesType.put(e.getKey(), deduceDeviceType(e.getValue())));
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) {
|
||||
} catch (SenseEnergyApiException e) {
|
||||
handleApiException(e);
|
||||
}
|
||||
}
|
||||
|
@ -538,11 +549,11 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
|
|||
.isPresent();
|
||||
|
||||
if (childOnline && !datagram.isRunning()) {
|
||||
datagram.stop();
|
||||
try {
|
||||
datagram.start(SENSE_DATAGRAM_BCAST_PORT, datagramListenerThreadName);
|
||||
} catch (IOException e) {
|
||||
handleApiException(e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
logger.warn("Unable to start datagram: {}", e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -604,15 +615,26 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
|
|||
|
||||
/***** SenseEnergyeWSListener interfaces *****/
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketClose(int statusCode, @Nullable String reason) {
|
||||
logger.debug("onWebSocketClose ({}), {}", statusCode, reason);
|
||||
// will restart on heartbeat
|
||||
try {
|
||||
webSocket.restartWithBackoff(getApi().getAccessToken());
|
||||
} catch (InterruptedException | ExecutionException | IOException | URISyntaxException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
logger.warn("Exeception when restarting webSocket: {}", e.getMessage());
|
||||
// will retry at next heartbeat
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketError(String msg) {
|
||||
// no action - let heartbeat restart webSocket
|
||||
logger.debug("onWebSocketError {}", msg);
|
||||
// restart will occur on onWebSocketClose
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -86,6 +86,7 @@ api.invalid-user-credentials = Invalid user credentials, please check configurat
|
|||
api.response-fail = API response fail
|
||||
api.response-invalid = API response invalid
|
||||
api.rate-limit-exceeded = API rate limit exceeded
|
||||
api.request-error = Error occurred during API request
|
||||
|
||||
# actions
|
||||
|
||||
|
|
Loading…
Reference in New Issue