diff --git a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/actions/SenseEnergyMonitorActions.java b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/actions/SenseEnergyMonitorActions.java index 0cac3a728c7..965fe1fde0e 100644 --- a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/actions/SenseEnergyMonitorActions.java +++ b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/actions/SenseEnergyMonitorActions.java @@ -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(); } diff --git a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyApi.java b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyApi.java index d50aeb4f3ae..28e6bb1e7ff 100644 --- a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyApi.java +++ b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyApi.java @@ -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 initialize(String email, String password) - throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException { + public Set 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 getDevices(long id) - throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException { + public Map 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 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 apiRequireNonNull(@Nullable T obj) throws SenseEnergyApiException { + if (obj == null) { + throw new SenseEnergyApiException("@text/api.response-invalid", SenseEnergyApiException.SEVERITY.TRANSIENT); + } else { + return obj; + } + } } diff --git a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyApiException.java b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyApiException.java index 3e38fdd9a63..6d438ba7f85 100644 --- a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyApiException.java +++ b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyApiException.java @@ -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()); } } diff --git a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyDatagram.java b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyDatagram.java index c91ed75e9e3..0cefa2a78d9 100644 --- a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyDatagram.java +++ b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyDatagram.java @@ -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; } } } diff --git a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyWebSocket.java b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyWebSocket.java index aea01a9dd53..4fbde5f1334 100644 --- a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyWebSocket.java +++ b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyWebSocket.java @@ -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()); } diff --git a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyWebSocketListener.java b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyWebSocketListener.java index 590c0cf6bce..bb38186b687 100644 --- a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyWebSocketListener.java +++ b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/api/SenseEnergyWebSocketListener.java @@ -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 * diff --git a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/handler/SenseEnergyBridgeHandler.java b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/handler/SenseEnergyBridgeHandler.java index 289a40cc311..2cfe4cd7eec 100644 --- a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/handler/SenseEnergyBridgeHandler.java +++ b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/handler/SenseEnergyBridgeHandler.java @@ -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()); } /* diff --git a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/handler/SenseEnergyMonitorHandler.java b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/handler/SenseEnergyMonitorHandler.java index c31769070dc..7e552a96058 100644 --- a/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/handler/SenseEnergyMonitorHandler.java +++ b/bundles/org.openhab.binding.senseenergy/src/main/java/org/openhab/binding/senseenergy/internal/handler/SenseEnergyMonitorHandler.java @@ -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 diff --git a/bundles/org.openhab.binding.senseenergy/src/main/resources/OH-INF/i18n/senseenergy.properties b/bundles/org.openhab.binding.senseenergy/src/main/resources/OH-INF/i18n/senseenergy.properties index 2e91b038959..ba8985b7961 100644 --- a/bundles/org.openhab.binding.senseenergy/src/main/resources/OH-INF/i18n/senseenergy.properties +++ b/bundles/org.openhab.binding.senseenergy/src/main/resources/OH-INF/i18n/senseenergy.properties @@ -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