[senseenergy] Address reconnect issues on failure (#18463)

* Address reconnect issues on failure

Signed-off-by: Jeff James <jeff@james-online.com>
pull/18636/head
jsjames 2025-05-02 14:47:48 -07:00 committed by GitHub
parent d86fea3104
commit 4b175571be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 202 additions and 180 deletions

View File

@ -18,8 +18,6 @@ import java.time.Instant;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import javax.measure.quantity.Dimensionless; import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Energy; import javax.measure.quantity.Energy;
@ -105,7 +103,7 @@ public class SenseEnergyMonitorActions implements ThingActions {
SenseEnergyApiGetTrends trends; SenseEnergyApiGetTrends trends;
try { try {
trends = localDeviceHandler.getApi().getTrendData(localDeviceHandler.getId(), trendScale, localDateTime); trends = localDeviceHandler.getApi().getTrendData(localDeviceHandler.getId(), trendScale, localDateTime);
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) { } catch (SenseEnergyApiException e) {
logger.warn("queryEnergyTrends function failed - {}", e.getMessage()); logger.warn("queryEnergyTrends function failed - {}", e.getMessage());
return Collections.emptyMap(); return Collections.emptyMap();
} }

View File

@ -37,6 +37,7 @@ import org.eclipse.jetty.client.util.FormContentProvider;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.Fields; 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.SenseEnergyApiAuthenticate;
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiDevice; import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiDevice;
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiGetTrends; 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 * implementation here: https://github.com/scottbonline/sense
* *
* @author Jeff James - Initial contribution * @author Jeff James - Initial contribution
*
*/ */
@NonNullByDefault @NonNullByDefault
public class SenseEnergyApi { public class SenseEnergyApi {
@ -119,17 +119,8 @@ public class SenseEnergyApi {
* @param password * @param password
* *
* @return a set of IDs for all the monitors associated with this account * @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) public Set<Long> initialize(String email, String password) throws SenseEnergyApiException {
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
Fields fields = new Fields(); Fields fields = new Fields();
fields.put("email", email); fields.put("email", email);
fields.put("password", password); fields.put("password", password);
@ -143,7 +134,7 @@ public class SenseEnergyApi {
SenseEnergyApiAuthenticate.class); SenseEnergyApiAuthenticate.class);
if (data == null) { if (data == null) {
throw new SenseEnergyApiException("@text/api.response-invalid", false); throw new SenseEnergyApiException("@text/api.response-invalid", SenseEnergyApiException.SEVERITY.FATAL);
} }
accessToken = data.accessToken; accessToken = data.accessToken;
@ -157,17 +148,8 @@ public class SenseEnergyApi {
/* /*
* renew authentication credentials. Timeout of credentials is ~24 hours. * renew authentication credentials. Timeout of credentials is ~24 hours.
*
* @throws InterruptedException
*
* @throws TimeoutException
*
* @throws ExecutionException
*
* @throws SenseEnergyApiException
*/ */
public void refreshToken() public void refreshToken() throws SenseEnergyApiException {
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
Fields fields = new Fields(); Fields fields = new Fields();
fields.add("user_id", Long.toString(this.userID)); fields.add("user_id", Long.toString(this.userID));
fields.add("refresh_token", this.refreshToken); fields.add("refresh_token", this.refreshToken);
@ -181,7 +163,7 @@ public class SenseEnergyApi {
SenseEnergyApiRefreshToken.class); SenseEnergyApiRefreshToken.class);
if (data == null) { 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); 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 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); Request request = httpClient.newRequest(APIURL_LOGOUT).method(HttpMethod.GET);
sendRequest(request); sendRequest(request);
@ -204,17 +186,8 @@ public class SenseEnergyApi {
* @param id of the monitor * @param id of the monitor
* *
* @return dto structure containing monitor info * @return dto structure containing monitor info
*
* @throws InterruptedException
*
* @throws TimeoutException
*
* @throws ExecutionException
*
* @throws SenseEnergyApiException
*/ */
public SenseEnergyApiMonitor getMonitorOverview(long id) public SenseEnergyApiMonitor getMonitorOverview(long id) throws SenseEnergyApiException {
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
String url = String.format(APIURL_MONITOR_OVERVIEW, id); String url = String.format(APIURL_MONITOR_OVERVIEW, id);
Request request = httpClient.newRequest(url).method(HttpMethod.GET); Request request = httpClient.newRequest(url).method(HttpMethod.GET);
@ -222,17 +195,11 @@ public class SenseEnergyApi {
try { try {
JsonObject jsonResponse = JsonParser.parseString(response.getContentAsString()).getAsJsonObject(); JsonObject jsonResponse = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
SenseEnergyApiMonitor monitor = gson.fromJson( return apiRequireNonNull(
jsonResponse.getAsJsonObject("monitor_overview").getAsJsonObject("monitor"), gson.fromJson(jsonResponse.getAsJsonObject("monitor_overview").getAsJsonObject("monitor"),
SenseEnergyApiMonitor.class); SenseEnergyApiMonitor.class));
if (monitor == null) {
throw new SenseEnergyApiException("@text/api.response-invalid", false);
}
return monitor;
} catch (JsonSyntaxException e) { } 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 * @param id - id of monitor
* *
* @return dto structure containing monitor status * @return dto structure containing monitor status
*
* @throws InterruptedException
*
* @throws TimeoutException
*
* @throws ExecutionException
*
* @throws SenseEnergyApiException
*/ */
@Nullable public SenseEnergyApiMonitorStatus getMonitorStatus(long id) throws SenseEnergyApiException {
public SenseEnergyApiMonitorStatus getMonitorStatus(long id)
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
String url = String.format(APIURL_MONITOR_STATUS, id); String url = String.format(APIURL_MONITOR_STATUS, id);
Request request = httpClient.newRequest(url).method(HttpMethod.GET); Request request = httpClient.newRequest(url).method(HttpMethod.GET);
ContentResponse response = sendRequest(request); ContentResponse response = sendRequest(request);
final SenseEnergyApiMonitorStatus data = gson.fromJson(response.getContentAsString(), return apiRequireNonNull(gson.fromJson(response.getContentAsString(), SenseEnergyApiMonitorStatus.class));
SenseEnergyApiMonitorStatus.class);
return data;
} }
@Nullable @Nullable
public SenseEnergyApiGetTrends getTrendData(long id, TrendScale trendScale) public SenseEnergyApiGetTrends getTrendData(long id, TrendScale trendScale) throws SenseEnergyApiException {
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
return getTrendData(id, trendScale, Instant.now()); 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 . * @param datetime a datetime within the scale of which to receive data. Does not need to be the start or end .
* *
* @return * @return
*
* @throws InterruptedException
*
* @throws TimeoutException
*
* @throws ExecutionException
*
* @throws SenseEnergyApiException
*/ */
@Nullable @Nullable
public SenseEnergyApiGetTrends getTrendData(long id, TrendScale trendScale, Instant datetime) 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()); String url = String.format(APIURL_GET_TRENDS, id, trendScale.toString(), datetime.toString());
Request request = httpClient.newRequest(url).method(HttpMethod.GET); Request request = httpClient.newRequest(url).method(HttpMethod.GET);
ContentResponse response = sendRequest(request); ContentResponse response = sendRequest(request);
final SenseEnergyApiGetTrends data = gson.fromJson(response.getContentAsString(), return gson.fromJson(response.getContentAsString(), SenseEnergyApiGetTrends.class);
SenseEnergyApiGetTrends.class);
return data;
} }
/* /*
@ -323,17 +265,8 @@ public class SenseEnergyApi {
* @param id of the monitor device * @param id of the monitor device
* *
* @return Map of discovered devices with the ID of the device as key and the dto object SenseEnergyApiDevice * @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) public Map<String, SenseEnergyApiDevice> getDevices(long id) throws SenseEnergyApiException {
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
String url = String.format(APIURL_GET_DEVICES, id); String url = String.format(APIURL_GET_DEVICES, id);
Request request = httpClient.newRequest(url).method(HttpMethod.GET); Request request = httpClient.newRequest(url).method(HttpMethod.GET);
@ -341,9 +274,6 @@ public class SenseEnergyApi {
JsonArray jsonDevices = JsonParser.parseString(response.getContentAsString()).getAsJsonArray(); 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<String, SenseEnergyApiDevice> mapDevices = StreamSupport.stream(jsonDevices.spliterator(), false) //
.map(j -> jsonToSenseEnergyDevice(j)) // .map(j -> jsonToSenseEnergyDevice(j)) //
.filter(Objects::nonNull) // .filter(Objects::nonNull) //
@ -362,28 +292,34 @@ public class SenseEnergyApi {
return request; return request;
} }
public void verifyToken() public void verifyToken() throws SenseEnergyApiException {
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
if (tokenExpiresAt.isBefore(Instant.now())) { if (tokenExpiresAt.isBefore(Instant.now())) {
refreshToken(); refreshToken();
} }
} }
ContentResponse sendRequest(Request request) ContentResponse sendRequest(Request request) throws SenseEnergyApiException {
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
return sendRequest(request, true); return sendRequest(request, true);
} }
ContentResponse sendRequest(Request request, boolean verifyToken) ContentResponse sendRequest(Request request, boolean verifyToken) throws SenseEnergyApiException {
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
if (verifyToken) { if (verifyToken) {
verifyToken(); verifyToken();
} }
setHeaders(request); setHeaders(request);
logger.trace("REQUEST: {}", request.toString()); ContentResponse response;
ContentResponse response = request.send(); 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()); logger.trace("RESPONSE: {}", response.getContentAsString());
switch (response.getStatus()) { switch (response.getStatus()) {
@ -391,13 +327,24 @@ public class SenseEnergyApi {
break; break;
case 400: // API responses with 400 when user credentials are invalid case 400: // API responses with 400 when user credentials are invalid
case 401: case 401:
throw new SenseEnergyApiException("@text/api.invalid-user-credentials", true); throw new SenseEnergyApiException("@text/api.invalid-user-credentials",
SenseEnergyApiException.SEVERITY.CONFIG);
case 429: case 429:
throw new SenseEnergyApiException("@text/api.rate-limit-exceeded", false); throw new SenseEnergyApiException("@text/api.rate-limit-exceeded",
SenseEnergyApiException.SEVERITY.TRANSIENT);
default: default:
throw new SenseEnergyApiException("Unexpected API error: " + response.getReason(), false); throw new SenseEnergyApiException("Unexpected API error: " + response.getReason(),
SenseEnergyApiException.SEVERITY.TRANSIENT);
} }
return response; 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;
}
}
} }

View File

@ -13,6 +13,7 @@
package org.openhab.binding.senseenergy.internal.api; package org.openhab.binding.senseenergy.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/** /**
* {@link SenseEnergyApiException} exception class for any api exception * {@link SenseEnergyApiException} exception class for any api exception
@ -22,20 +23,33 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
@NonNullByDefault @NonNullByDefault
public class SenseEnergyApiException extends Exception { public class SenseEnergyApiException extends Exception {
private static final long serialVersionUID = -7059398508028583720L; 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) { public static enum SEVERITY {
super(message); CONFIG,
this.configurationIssue = configurationIssue; TRANSIENT,
DATA,
FATAL
} }
public boolean isConfigurationIssue() { public SenseEnergyApiException(String message, SEVERITY severity) {
return configurationIssue; 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 @Override
public String toString() { public String toString() {
return String.format("SenseEnergyApiException{message='%s', configurationIssue=%b}", getMessage(), Exception localE = e;
configurationIssue); return String.format("SenseEnergyApiException{message='%s', severity=%s}",
(localE == null) ? getMessage() : localE.getMessage(), severity.toString());
} }
} }

View File

@ -16,6 +16,7 @@ import java.io.IOException;
import java.net.DatagramPacket; import java.net.DatagramPacket;
import java.net.DatagramSocket; import java.net.DatagramSocket;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.nio.channels.ClosedByInterruptException;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -78,17 +79,12 @@ public class SenseEnergyDatagram {
} }
public void stop() { public void stop() {
connected = false; logger.debug("datagram stop");
Thread localUdpThread = udpListener;
try { if (localUdpThread != null) {
DatagramSocket localSocket = datagramSocket; connected = false;
if (localSocket != null) { localUdpThread.interrupt();
localSocket.close(); udpListener = null;
datagramSocket = null;
logger.debug("Closing datagram listener");
}
} catch (Exception exception) {
logger.debug("closeConnection(): Error closing connection - {}", exception.getMessage());
} }
} }
@ -103,7 +99,6 @@ public class SenseEnergyDatagram {
} }
private class UDPListener implements Runnable { private class UDPListener implements Runnable {
/* /*
* Run method. Runs the MessageListener thread * Run method. Runs the MessageListener thread
*/ */
@ -121,41 +116,48 @@ public class SenseEnergyDatagram {
DatagramPacket packet = new DatagramPacket(new byte[BUFFERSIZE], BUFFERSIZE); DatagramPacket packet = new DatagramPacket(new byte[BUFFERSIZE], BUFFERSIZE);
while (connected) { try {
try { while (connected && !localSocket.isClosed() && !Thread.currentThread().isInterrupted()) {
localSocket.receive(packet);
} catch (IOException e) {
logger.debug("Exception during packet read - {}", e.getMessage());
try { 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 Thread.sleep(100); // allow CPU to breath
} catch (InterruptedException ie) { continue;
Thread.currentThread().interrupt();
} }
break;
}
// don't receive more than 1 request a second. Necessary to filter out receiving the same // 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 // broadcast request packet on multiple interfaces (i.e. wi-fi and wired) at the same time
if (System.nanoTime() < nextPacketTime) { if (System.nanoTime() < nextPacketTime) {
continue; continue;
} }
JsonObject jsonResponse; JsonObject jsonResponse;
String decryptedPacket = new String(TpLinkEncryption.decrypt(packet.getData(), packet.getLength())); String decryptedPacket = new String(TpLinkEncryption.decrypt(packet.getData(), packet.getLength()));
try { try {
jsonResponse = JsonParser.parseString(decryptedPacket).getAsJsonObject(); jsonResponse = JsonParser.parseString(decryptedPacket).getAsJsonObject();
} catch (JsonSyntaxException jsonSyntaxException) { } catch (JsonSyntaxException jsonSyntaxException) {
logger.trace("Invalid JSON received"); logger.trace("Invalid JSON received");
continue; continue;
} }
nextPacketTime = System.nanoTime() + 1000000000L; nextPacketTime = System.nanoTime() + 1000000000L;
if (jsonResponse.has("system") && jsonResponse.has("emeter")) { if (jsonResponse.has("system") && jsonResponse.has("emeter")) {
SenseEnergyDatagramListener localPacketListener = packetListener; SenseEnergyDatagramListener localPacketListener = packetListener;
if (localPacketListener != null) { if (localPacketListener != null) {
localPacketListener.requestReceived(packet.getSocketAddress()); localPacketListener.requestReceived(packet.getSocketAddress());
}
} }
} }
} catch (InterruptedException | ClosedByInterruptException e) {
Thread.currentThread().interrupt();
} finally {
localSocket.close();
datagramSocket = null;
connected = false;
} }
} }
} }

View File

@ -12,9 +12,12 @@
*/ */
package org.openhab.binding.senseenergy.internal.api; package org.openhab.binding.senseenergy.internal.api;
import static org.openhab.binding.senseenergy.internal.SenseEnergyBindingConstants.HEARTBEAT_MINUTES;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.time.Duration;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@ -51,6 +54,10 @@ public class SenseEnergyWebSocket implements WebSocketListener {
private boolean closing; private boolean closing;
private long monitorId; 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(); private Gson gson = new Gson();
public boolean isClosing() { public boolean isClosing() {
@ -62,7 +69,8 @@ public class SenseEnergyWebSocket implements WebSocketListener {
this.client = client; 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); logger.debug("Starting Sense Energy WebSocket for monitor ID: {}", monitorId);
this.monitorId = monitorId; this.monitorId = monitorId;
@ -71,16 +79,26 @@ public class SenseEnergyWebSocket implements WebSocketListener {
} }
public void restart(String accessToken) public void restart(String accessToken)
throws InterruptedException, ExecutionException, IOException, URISyntaxException, Exception { throws InterruptedException, ExecutionException, IOException, URISyntaxException {
logger.debug("Re-starting Sense Energy WebSocket"); logger.debug("Re-starting Sense Energy WebSocket");
stop(); stop();
start(monitorId, accessToken); 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() { public synchronized void stop() {
closing = true; closing = true;
logger.trace("Stopping Sense Energy WebSocket"); logger.debug("Stopping Sense Energy WebSocket");
WebSocketSession localSession = session; WebSocketSession localSession = session;
if (localSession != null) { if (localSession != null) {
@ -115,6 +133,7 @@ public class SenseEnergyWebSocket implements WebSocketListener {
public void onWebSocketConnect(@Nullable Session session) { public void onWebSocketConnect(@Nullable Session session) {
closing = false; closing = false;
logger.debug("Connected to Sense Energy WebSocket"); logger.debug("Connected to Sense Energy WebSocket");
listener.onWebSocketConnect();
} }
@Override @Override
@ -139,6 +158,8 @@ public class SenseEnergyWebSocket implements WebSocketListener {
return; return;
} }
logger.debug("onWebSocketText");
try { try {
JsonObject jsonResponse = JsonParser.parseString(message).getAsJsonObject(); JsonObject jsonResponse = JsonParser.parseString(message).getAsJsonObject();
String type = jsonResponse.get("type").getAsString(); String type = jsonResponse.get("type").getAsString();
@ -150,6 +171,9 @@ public class SenseEnergyWebSocket implements WebSocketListener {
if (update != null) { if (update != null) {
listener.onWebSocketRealtimeUpdate(update); 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)) { } else if ("error".equals(type)) {
logger.warn("WebSocket error {}", jsonResponse.get("payload").toString()); logger.warn("WebSocket error {}", jsonResponse.get("payload").toString());
} }

View File

@ -23,6 +23,11 @@ import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyWebSocketReal
*/ */
@NonNullByDefault @NonNullByDefault
public interface SenseEnergyWebSocketListener { public interface SenseEnergyWebSocketListener {
/**
* called when web socket connects
*/
void onWebSocketConnect();
/** /**
* called when the web socket is closed * called when the web socket is closed
* *

View File

@ -18,10 +18,8 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -92,7 +90,7 @@ public class SenseEnergyBridgeHandler extends BaseBridgeHandler {
public void goOnline() { public void goOnline() {
try { try {
this.monitorIDs = api.initialize(config.email, config.password); this.monitorIDs = api.initialize(config.email, config.password);
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) { } catch (SenseEnergyApiException e) {
handleApiException(e); handleApiException(e);
return; return;
} }
@ -105,6 +103,7 @@ public class SenseEnergyBridgeHandler extends BaseBridgeHandler {
} }
private void heartbeat() { private void heartbeat() {
logger.trace("heartbeat");
ThingStatus thingStatus = getThing().getStatus(); ThingStatus thingStatus = getThing().getStatus();
if (thingStatus == ThingStatus.OFFLINE 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 // token is verified on each api call, called here in case no API calls are made in the alloted period
try { try {
getApi().verifyToken(); getApi().verifyToken();
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) { } catch (SenseEnergyApiException e) {
handleApiException(e); handleApiException(e);
} }
@ -133,17 +132,27 @@ public class SenseEnergyBridgeHandler extends BaseBridgeHandler {
} }
public void handleApiException(Exception e) { public void handleApiException(Exception e) {
ThingStatusDetail statusDetail = ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR;
if (e instanceof SenseEnergyApiException apiException) { if (e instanceof SenseEnergyApiException apiException) {
statusDetail = apiException.isConfigurationIssue() ? ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR switch (apiException.severity) {
: ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR; 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 { } else {
logger.debug("Unhandled Exception", e); logger.warn("Unhandled Exception", e);
statusDetail = ThingStatusDetail.OFFLINE.NONE;
} }
updateStatus(ThingStatus.OFFLINE, statusDetail, e.getLocalizedMessage());
} }
/* /*

View File

@ -16,6 +16,7 @@ import static org.openhab.binding.senseenergy.internal.SenseEnergyBindingConstan
import java.io.IOException; import java.io.IOException;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.URISyntaxException;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -26,7 +27,6 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.measure.Unit; import javax.measure.Unit;
@ -170,7 +170,7 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
this.solarConfigured = apiMonitor.solarConfigured; this.solarConfigured = apiMonitor.solarConfigured;
apiMonitorStatus = getApi().getMonitorStatus(id); apiMonitorStatus = getApi().getMonitorStatus(id);
refreshDevices(); refreshDevices();
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) { } catch (SenseEnergyApiException e) {
handleApiException(e); handleApiException(e);
return; return;
} }
@ -192,7 +192,7 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
try { try {
webSocket.start(id, getApi().getAccessToken()); webSocket.start(id, getApi().getAccessToken());
} catch (Exception e) { } catch (InterruptedException | ExecutionException | IOException | URISyntaxException e) {
handleApiException(e); handleApiException(e);
return; return;
} }
@ -221,7 +221,7 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
logger.debug("heartbeat: webSocket not running"); logger.debug("heartbeat: webSocket not running");
try { try {
webSocket.restart(getApi().getAccessToken()); webSocket.restart(getApi().getAccessToken());
} catch (Exception e) { } catch (InterruptedException | ExecutionException | IOException | URISyntaxException e) {
handleApiException(e); handleApiException(e);
} }
} }
@ -230,17 +230,27 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
} }
public void handleApiException(Exception e) { public void handleApiException(Exception e) {
ThingStatusDetail statusDetail = ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR;
if (e instanceof SenseEnergyApiException apiException) { if (e instanceof SenseEnergyApiException apiException) {
statusDetail = apiException.isConfigurationIssue() ? ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR switch (apiException.severity) {
: ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR; 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 { } else {
logger.debug("Unhandled Exception", e); logger.warn("Unhandled Exception", e);
statusDetail = ThingStatusDetail.OFFLINE.NONE;
} }
updateStatus(ThingStatus.OFFLINE, statusDetail, e.getLocalizedMessage());
} }
@Override @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. * Refreshes the list of devices by retrieving them from the API and then updating the map of DeviceTypes.
*/ */
private void refreshDevices() { private void refreshDevices() {
logger.trace("refreshDevices");
try { try {
senseDevices = getApi().getDevices(id); senseDevices = getApi().getDevices(id);
senseDevices.entrySet().stream() // senseDevices.entrySet().stream() //
.filter(e -> !senseDevicesType.containsKey(e.getKey())) // .filter(e -> !senseDevicesType.containsKey(e.getKey())) //
.forEach(e -> senseDevicesType.put(e.getKey(), deduceDeviceType(e.getValue()))); .forEach(e -> senseDevicesType.put(e.getKey(), deduceDeviceType(e.getValue())));
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) { } catch (SenseEnergyApiException e) {
handleApiException(e); handleApiException(e);
} }
} }
@ -538,11 +549,11 @@ public class SenseEnergyMonitorHandler extends BaseBridgeHandler
.isPresent(); .isPresent();
if (childOnline && !datagram.isRunning()) { if (childOnline && !datagram.isRunning()) {
datagram.stop();
try { try {
datagram.start(SENSE_DATAGRAM_BCAST_PORT, datagramListenerThreadName); datagram.start(SENSE_DATAGRAM_BCAST_PORT, datagramListenerThreadName);
} catch (IOException e) { } 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 *****/ /***** SenseEnergyeWSListener interfaces *****/
@Override
public void onWebSocketConnect() {
}
@Override @Override
public void onWebSocketClose(int statusCode, @Nullable String reason) { public void onWebSocketClose(int statusCode, @Nullable String reason) {
logger.debug("onWebSocketClose ({}), {}", statusCode, 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 @Override
public void onWebSocketError(String msg) { public void onWebSocketError(String msg) {
// no action - let heartbeat restart webSocket logger.debug("onWebSocketError {}", msg);
// restart will occur on onWebSocketClose
} }
@Override @Override

View File

@ -86,6 +86,7 @@ api.invalid-user-credentials = Invalid user credentials, please check configurat
api.response-fail = API response fail api.response-fail = API response fail
api.response-invalid = API response invalid api.response-invalid = API response invalid
api.rate-limit-exceeded = API rate limit exceeded api.rate-limit-exceeded = API rate limit exceeded
api.request-error = Error occurred during API request
# actions # actions