[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.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();
}

View File

@ -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;
}
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}
}

View File

@ -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());
}

View File

@ -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
*

View File

@ -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());
}
/*

View File

@ -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

View File

@ -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