[shelly] Fix Shelly2RpcSocket concurrency issues (#20069)

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
This commit is contained in:
Nadahar
2026-02-27 22:19:52 +01:00
committed by GitHub
parent 279b7595d9
commit f10e288429
18 changed files with 947 additions and 346 deletions
@@ -20,7 +20,9 @@ import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.api2.Shelly2RpcSocket;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyBluHandler;
@@ -33,6 +35,7 @@ import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.binding.shelly.internal.util.ShellyUtils;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.WebSocketFactory;
import org.openhab.core.net.HttpServiceUtil;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Thing;
@@ -41,8 +44,10 @@ import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.ComponentException;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -60,6 +65,7 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
private final ShellyTranslationProvider messages;
private final Shelly1CoapServer coapServer;
private final ShellyThingTable thingTable;
private final WebSocketClient webSocketClient;
private ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
/**
@@ -72,11 +78,19 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
@Activate
public ShellyHandlerFactory(@Reference NetworkAddressService networkAddressService,
@Reference ShellyTranslationProvider translationProvider, @Reference ShellyThingTable thingTable,
@Reference HttpClientFactory httpClientFactory, ComponentContext componentContext,
Map<String, Object> configProperties) {
@Reference HttpClientFactory httpClientFactory, @Reference WebSocketFactory webSocketFactory,
ComponentContext componentContext, Map<String, Object> configProperties) {
super.activate(componentContext);
this.messages = translationProvider;
this.thingTable = thingTable;
WebSocketClient client = Shelly2RpcSocket.createWebSocketClient(webSocketFactory, "shelly2api");
this.webSocketClient = client;
try {
client.start();
} catch (Exception e) {
logger.error("Failed to start ShellyHandlerFactory WebSocketClient: {}", e.getMessage(), e);
throw new ComponentException("Failed to activate: Unable to start WebSocket client: " + e.getMessage(), e);
}
bindingConfig.updateFromProperties(configProperties);
String localIP = bindingConfig.localIP;
@@ -97,11 +111,16 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
bindingConfig.httpPort = httpPort;
this.coapServer = new Shelly1CoapServer();
this.thingTable.startDiscoveryService(bundleContext);
}
@Activate
void activate() {
thingTable.startDiscoveryService(bundleContext);
@Deactivate
public void deactivate() {
try {
webSocketClient.stop();
} catch (Exception e) {
logger.warn("Failed to stop ShellyHandlerFactory WebSocketClient: {}", e.getMessage(), e);
}
}
@Override
@@ -117,19 +136,23 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
if (THING_TYPE_SHELLYPROTECTED.equals(thingTypeUID)) {
logger.debug("{}: Create new thing of type {} using ShellyProtectedHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyProtectedHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
handler = new ShellyProtectedHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient,
webSocketClient);
} else if (GROUP_LIGHT_THING_TYPES.contains(thingTypeUID)) {
logger.debug("{}: Create new thing of type {} using ShellyLightHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyLightHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
handler = new ShellyLightHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient,
webSocketClient);
} else if (GROUP_BLU_THING_TYPES.contains(thingTypeUID)) {
logger.debug("{}: Create new thing of type {} using ShellyBluSensorHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyBluHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
handler = new ShellyBluHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient,
webSocketClient);
} else if (SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
logger.debug("{}: Create new thing of type {} using ShellyRelayHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyRelayHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
handler = new ShellyRelayHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient,
webSocketClient);
}
if (handler != null) {
@@ -166,7 +189,7 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
public void onEvent(String ipAddress, String deviceName, String componentIndex, String eventType,
Map<String, String> parameters) {
logger.trace("{}: Dispatch event to thing handler", deviceName);
for (Map.Entry<String, ShellyThingInterface> listener : thingTable.getTable().entrySet()) {
for (Map.Entry<String, ShellyThingInterface> listener : thingTable.getAll().entrySet()) {
ShellyBaseHandler thingHandler = (ShellyBaseHandler) listener.getValue();
if (thingHandler.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) {
// event processed
@@ -181,7 +204,7 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
public Map<String, ShellyManagerInterface> getThingHandlers() {
Map<String, ShellyManagerInterface> table = new HashMap<>();
for (Map.Entry<String, ShellyThingInterface> entry : thingTable.getTable().entrySet()) {
for (Map.Entry<String, ShellyThingInterface> entry : thingTable.getAll().entrySet()) {
table.put(entry.getKey(), (ShellyManagerInterface) entry.getValue());
}
return table;
@@ -15,10 +15,8 @@ package org.openhab.binding.shelly.internal.api;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
@@ -27,7 +25,6 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLi
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link ShellyApiInterface} Defines device API
@@ -35,18 +32,11 @@ import org.openhab.core.thing.ThingTypeUID;
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public interface ShellyApiInterface {
public interface ShellyApiInterface extends ShellyDiscoveryInterface {
boolean isInitialized();
void initialize() throws ShellyApiException;
void setConfig(String thingName, ShellyThingConfiguration config);
ShellySettingsDevice getDeviceInfo() throws ShellyApiException;
ShellyDeviceProfile getDeviceProfile(ThingTypeUID thingTypeUID, @Nullable ShellySettingsDevice device)
throws ShellyApiException;
ShellySettingsStatus getStatus() throws ShellyApiException;
void setLedStatus(String ledName, boolean value) throws ShellyApiException;
@@ -141,7 +131,5 @@ public interface ShellyApiInterface {
void postEvent(String device, String index, String event, Map<String, String> parms) throws ShellyApiException;
void close();
void startScan();
}
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2026 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link ShellyDiscoveryInterface} defines the API necessary for discovery.
*
* @author Ravi Nadahar - Initial contribution
*/
@NonNullByDefault
public interface ShellyDiscoveryInterface {
void initialize() throws ShellyApiException;
ShellySettingsDevice getDeviceInfo() throws ShellyApiException;
ShellyDeviceProfile getDeviceProfile(ThingTypeUID thingTypeUID, @Nullable ShellySettingsDevice device)
throws ShellyApiException;
void close();
}
@@ -20,6 +20,7 @@ import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Base64;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -63,24 +64,26 @@ public class ShellyHttpClient {
public static final String CONTENT_TYPE_FORM_URLENC = "application/x-www-form-urlencoded";
protected final HttpClient httpClient;
protected ShellyThingConfiguration config = new ShellyThingConfiguration();
protected String thingName;
protected ShellyThingConfiguration config;
protected volatile String thingName;
protected final Gson gson = new Gson();
protected int timeoutErrors = 0;
protected int timeoutsRecovered = 0;
private ShellyDeviceProfile profile;
private final ShellyDeviceProfile profile;
protected boolean basicAuth = false;
public ShellyHttpClient(String thingName, ShellyThingInterface thing) {
this(thingName, thing.getThingConfig(), thing.getHttpClient());
this.thingName = thingName;
this.config = thing.getThingConfig();
this.httpClient = thing.getHttpClient();
this.profile = thing.getProfile();
}
public ShellyHttpClient(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
profile = new ShellyDeviceProfile();
this.thingName = thingName;
setConfig(thingName, config);
this.config = config;
this.httpClient = httpClient;
this.profile = new ShellyDeviceProfile();
}
public void setConfig(String thingName, ShellyThingConfiguration config) {
@@ -233,7 +236,9 @@ public class ShellyHttpClient {
}
if (!SHELLY2_AUTHTTYPE_DIGEST.equalsIgnoreCase(challenge.authType)
|| !SHELLY2_AUTHALG_SHA256.equalsIgnoreCase(challenge.algorithm)) {
throw new IllegalArgumentException("Unsupported Auth type/algorithm requested by device");
throw new IllegalArgumentException(
String.format(Locale.ROOT, "Unsupported Auth type (%s) or algorithm (%s) requested by device",
challenge.authType, challenge.algorithm));
}
Shelly2AuthRsp response = new Shelly2AuthRsp();
response.username = user;
@@ -10,13 +10,14 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.api;
package org.openhab.binding.shelly.internal.api1;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
@@ -28,14 +29,7 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api2.Shelly2RpcSocket;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
@@ -45,49 +39,41 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ShellyEventServlet} implements the WebSocket callback for Gen2 devices
* {@link Shelly1EventServlet} implements the HttpSocket callback for Gen1 devices
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@WebServlet(name = "ShellyEventServlet", urlPatterns = { SHELLY1_CALLBACK_URI, SHELLY2_CALLBACK_URI })
@WebServlet(name = "Shelly1EventServlet", urlPatterns = { SHELLY1_CALLBACK_URI })
@Component(service = HttpServlet.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
public class ShellyEventServlet extends WebSocketServlet {
public class Shelly1EventServlet extends HttpServlet {
private static final long serialVersionUID = -1210354558091063207L;
private final Logger logger = LoggerFactory.getLogger(ShellyEventServlet.class);
private final Logger logger = LoggerFactory.getLogger(Shelly1EventServlet.class);
private final ShellyHandlerFactory handlerFactory;
private final ShellyThingTable thingTable;
@Activate
public ShellyEventServlet(@Reference ShellyHandlerFactory handlerFactory, @Reference ShellyThingTable thingTable) {
public Shelly1EventServlet(@Reference ShellyHandlerFactory handlerFactory) {
this.handlerFactory = handlerFactory;
this.thingTable = thingTable;
logger.debug("Shelly EventServlet started at {} and {}", SHELLY1_CALLBACK_URI, SHELLY2_CALLBACK_URI);
logger.debug("Shelly1EventServlet started at {}", SHELLY1_CALLBACK_URI);
}
@Deactivate
protected void deactivate() {
logger.debug("ShellyEventServlet: Stopping");
logger.debug("Shelly1EventServlet: Stopping");
}
/**
* Servlet handler. Shelly1: http request, Shelly2: WebSocket call
* Servlet handler. HTTP request.
*/
@Override
protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse resp)
throws ServletException, IOException, IllegalArgumentException {
if (request == null) {
logger.trace("ShellyEventServlet.service unexpectedly received a null request. Request not processed");
return;
}
String path = getString(request.getRequestURI()).toLowerCase();
if (path.equals(SHELLY2_CALLBACK_URI)) { // Shelly2 WebSocket
if (resp != null) {
super.service(request, resp);
}
logger.trace("Shelly1EventServlet.service unexpectedly received a null request. Request not processed");
return;
}
String path = getString(request.getRequestURI()).toLowerCase(Locale.ROOT);
// Shelly1: http events, URL looks like
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/relay/n?xxxxx or
@@ -99,10 +85,10 @@ public class ShellyEventServlet extends WebSocketServlet {
try {
String ipAddress = request.getRemoteAddr();
Map<String, String[]> parameters = request.getParameterMap();
logger.debug("ShellyEventServlet: {} Request from {}:{}{}?{}", request.getProtocol(), ipAddress,
logger.debug("Shelly1EventServlet: {} Request from {}:{}{}?{}", request.getProtocol(), ipAddress,
request.getRemotePort(), path, parameters.toString());
if (!path.toLowerCase().startsWith(SHELLY1_CALLBACK_URI) || !path.contains("/event/shelly")) {
logger.warn("ShellyEventServlet received unknown request: path = {}", path);
logger.warn("Shelly1EventServlet received unknown request: path = {}", path);
return;
}
@@ -133,39 +119,4 @@ public class ShellyEventServlet extends WebSocketServlet {
}
}
}
/*
* @Override
* public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
* {
* response.getWriter().println("HTTP GET method not implemented.");
* }
*/
/**
* WebSocket: register Shelly2RpcSocket class
*/
@Override
public void configure(@Nullable WebSocketServletFactory factory) {
if (factory != null) {
factory.getPolicy().setIdleTimeout(15000);
factory.setCreator(new Shelly2WebSocketCreator(thingTable));
factory.register(Shelly2RpcSocket.class);
}
}
public static class Shelly2WebSocketCreator implements WebSocketCreator {
private final Logger logger = LoggerFactory.getLogger(Shelly2WebSocketCreator.class);
private final ShellyThingTable thingTable;
public Shelly2WebSocketCreator(ShellyThingTable thingTable) {
this.thingTable = thingTable;
}
@Override
public Object createWebSocket(@Nullable ServletUpgradeRequest req, @Nullable ServletUpgradeResponse resp) {
logger.debug("WebSocket: Create socket from servlet");
return new Shelly2RpcSocket(thingTable, true);
}
}
}
@@ -12,7 +12,8 @@
*/
package org.openhab.binding.shelly.internal.api2;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.CHANNEL_INPUT;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.ShellyDevices.THING_TYPE_CAP_NUM_METERS;
import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
@@ -27,12 +28,16 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyDiscoveryInterface;
import org.openhab.binding.shelly.internal.api.ShellyHttpClient;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyFavPos;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorSleepMode;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorTmp;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
@@ -42,6 +47,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettings
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRgbwLight;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRoller;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsWiFiNetwork;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
@@ -55,14 +61,17 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSe
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorBat;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorHum;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorLux;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthChallenge;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRsp;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2CBStatus;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigCover;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigInput;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigPm1;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigSwitch;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DeviceConfigSta;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.ShellyDeviceConfigCB;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceSettings;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusLight;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2CoverStatus;
@@ -83,6 +92,7 @@ import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyComponents;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -93,7 +103,7 @@ import org.slf4j.LoggerFactory;
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class Shelly2ApiClient extends ShellyHttpClient {
public class Shelly2ApiClient extends ShellyHttpClient implements ShellyDiscoveryInterface {
private final Logger logger = LoggerFactory.getLogger(Shelly2ApiClient.class);
protected final Random random = new Random();
protected final ShellyStatusRelay relayStatus = new ShellyStatusRelay();
@@ -168,6 +178,270 @@ public class Shelly2ApiClient extends ShellyHttpClient {
SHELLY2_PROFILE_RGB, SHELLY_MODE_COLOR, //
SHELLY2_PROFILE_RGBW, SHELLY_MODE_COLOR);
@Override
public void initialize() throws ShellyApiException {
}
@Override
public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
Shelly2DeviceSettings device = callApi("/shelly", Shelly2DeviceSettings.class);
ShellySettingsDevice info = new ShellySettingsDevice();
info.hostname = getString(device.id);
info.name = getString(device.name);
info.fw = getString(device.fw);
info.type = getString(device.model);
info.mac = getString(device.mac);
info.auth = getBool(device.auth);
info.gen = getInteger(device.gen);
info.mode = mapValue(MAP_PROFILE, getString(device.profile));
return info;
}
@Override
public ShellyDeviceProfile getDeviceProfile(ThingTypeUID thingTypeUID, @Nullable ShellySettingsDevice devInfo)
throws ShellyApiException {
/*
* This is a slightly simplified version of the method with the same name in Shelly2ApiRpc,
* which is only used for discovery.
*
* TODO: Either simplify this much more, to acquire the information necessary for discovery,
* or reorganize the two methods so that Shelly2ApiRpc.getDeviceProfile() calls super (this method)
* first, and then takes only the extra steps that depends on other methods in Shelly2ApiRpc.
*/
ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
if (devInfo != null) {
profile.device = devInfo;
}
if (profile.device.type == null) {
profile.device = getDeviceInfo();
}
Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class);
profile.settingsJson = gson.toJson(dc);
profile.thingName = thingName;
profile.settings.name = profile.status.name = dc.sys.device.name;
profile.name = getString(profile.settings.name);
profile.settings.timezone = getString(dc.sys.location.tz);
profile.settings.discoverable = getBool(dc.sys.device.discoverable);
if (dc.wifi != null && dc.wifi.ap != null && dc.wifi.ap.rangeExtender != null) {
profile.settings.wifiAp.rangeExtender = getBool(dc.wifi.ap.rangeExtender.enable);
}
if (dc.cloud != null) {
profile.settings.cloud.enabled = getBool(dc.cloud.enable);
}
if (dc.mqtt != null) {
profile.settings.mqtt.enable = getBool(dc.mqtt.enable);
}
if (dc.sys.sntp != null) {
profile.settings.sntp.server = dc.sys.sntp.server;
}
profile.isRoller = dc.cover0 != null;
profile.isCB = dc.cb0 != null || dc.cb1 != null || dc.cb2 != null || dc.cb3 != null;
profile.settings.relays = !profile.isCB ? fillRelaySettings(profile, dc) : fillBreakerSettings(profile, dc);
profile.settings.inputs = fillInputSettings(profile, dc);
profile.settings.rollers = fillRollerSettings(profile, dc);
profile.isEMeter = true;
List<ShellySettingsInput> inputs = profile.settings.inputs;
profile.numInputs = inputs != null ? inputs.size() : 0;
List<ShellySettingsRelay> relays = profile.settings.relays;
profile.numRelays = relays != null ? relays.size() : 0;
List<ShellySettingsRoller> rollers = profile.settings.rollers;
profile.numRollers = rollers != null ? rollers.size() : 0;
profile.hasRelays = profile.numRelays > 0 || profile.numRollers > 0;
ShellySettingsDevice device = profile.device;
if (config.serviceName.isBlank()) {
config.serviceName = getString(profile.device.hostname);
logger.trace("{}: {} is used as serviceName", thingName, config.serviceName);
}
profile.settings.fw = getString(device.fw);
profile.fwDate = substringBefore(substringBefore(device.fw, "/"), "-");
profile.fwVersion = profile.status.update.oldVersion = ShellyDeviceProfile
.extractFwVersion(profile.settings.fw);
profile.status.hasUpdate = profile.status.update.hasUpdate = false;
if (dc.eth != null) {
profile.settings.ethernet = getBool(dc.eth.enable);
}
if (dc.ble != null) {
profile.settings.bluetooth = getBool(dc.ble.enable);
}
profile.settings.wifiSta = new ShellySettingsWiFiNetwork();
profile.settings.wifiSta1 = new ShellySettingsWiFiNetwork();
fillWiFiSta(dc.wifi.sta, profile.settings.wifiSta);
fillWiFiSta(dc.wifi.sta1, profile.settings.wifiSta1);
if (dc.wifi.ap != null && dc.wifi.ap.rangeExtender != null) {
profile.settings.rangeExtender = getBool(dc.wifi.ap.rangeExtender.enable);
}
profile.numMeters = 0;
if (profile.hasRelays) {
profile.status.relays = new ArrayList<>();
relayStatus.relays = new ArrayList<>();
profile.numMeters = profile.isRoller ? profile.numRollers : profile.numRelays;
for (int i = 0; i < profile.numRelays; i++) {
profile.status.relays.add(new ShellySettingsRelay());
relayStatus.relays.add(new ShellyShortStatusRelay());
}
}
if (profile.numInputs > 0) {
profile.status.inputs = new ArrayList<>();
relayStatus.inputs = new ArrayList<>();
for (int i = 0; i < profile.numInputs; i++) {
ShellyInputState input = new ShellyInputState(i);
profile.status.inputs.add(input);
relayStatus.inputs.add(input);
}
}
// handle special cases, because there is no indicator for a meter in GetConfig
// Pro 3EM has 3 meters
// Pro 2 has 2 relays, but no meters
// Mini PM has 1 meter, but no relay
Integer numMeters = THING_TYPE_CAP_NUM_METERS.get(thingTypeUID);
if (numMeters != null) {
profile.numMeters = numMeters;
} else if (dc.pm10 != null) {
profile.numMeters = 1;
} else if (dc.em0 != null) {
profile.numMeters = 3;
} else if (dc.em10 != null) {
profile.numMeters = 2;
}
if (profile.numMeters > 0) {
profile.status.meters = new ArrayList<>();
profile.status.emeters = new ArrayList<>();
relayStatus.meters = new ArrayList<>();
for (int i = 0; i < profile.numMeters; i++) {
profile.status.meters.add(new ShellySettingsMeter());
profile.status.emeters.add(new ShellySettingsEMeter());
relayStatus.meters.add(new ShellySettingsMeter());
}
}
if (profile.settings.inputs != null) {
relayStatus.inputs = new ArrayList<>();
for (int i = 0; i < profile.numInputs; i++) {
relayStatus.inputs.add(new ShellyInputState(0));
}
}
if (profile.isRoller) {
profile.status.rollers = new ArrayList<>();
for (int i = 0; i < profile.numRollers; i++) {
ShellyRollerStatus rs = new ShellyRollerStatus();
profile.status.rollers.add(rs);
rollerStatus.add(rs);
}
}
if (profile.isDimmer) {
ArrayList<@Nullable ShellySettingsDimmer> dimmers = new ArrayList<>();
dimmers.add(new ShellySettingsDimmer());
profile.settings.dimmers = dimmers;
profile.status.dimmers = new ArrayList<>();
profile.status.dimmers.add(new ShellyShortLightStatus());
fillDimmerSettings(profile, dc);
}
profile.status.lights = profile.isBulb ? new ArrayList<>() : null;
if (profile.isRGBW2) {
ArrayList<@Nullable ShellySettingsRgbwLight> rgbwLights = new ArrayList<>();
rgbwLights.add(new ShellySettingsRgbwLight());
profile.settings.lights = rgbwLights;
profile.status.lights = new ArrayList<>();
profile.status.lights.add(new ShellySettingsLight());
fillRgbwSettings(profile, dc);
}
profile.status.thermostats = profile.isTRV ? new ArrayList<>() : null;
if (profile.hasBattery) {
profile.settings.sleepMode = new ShellySensorSleepMode();
profile.settings.sleepMode.unit = "m";
profile.settings.sleepMode.period = dc.sys.sleep != null ? dc.sys.sleep.wakeupPeriod / 60 : 720;
}
if (dc.led != null) {
profile.settings.ledStatusDisable = !getBool(dc.led.sysLedEnable);
profile.settings.ledPowerDisable = "off".equals(getString(dc.led.powerLed));
}
profile.initialized = true;
return profile;
}
public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
// TODO: Unification with the method of the same name in Shelly2ApiRpc should be done. This is now a slightly
// modified version suitable for discovery only.
String json = "";
Shelly2RpcBaseMessage req = buildRequest(method, params);
try {
json = httpPost((Shelly2AuthChallenge) null, gson.toJson(req));
} catch (ShellyApiException e) {
ShellyApiResult res = e.getApiResult();
String auth = getString(res.authChallenge);
if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) {
String[] options = auth.split(",");
Shelly2AuthChallenge authInfo = new Shelly2AuthChallenge();
for (String o : options) {
String key = substringBefore(o, "=").stripLeading().trim();
String value = substringAfter(o, "=").replace("\"", "").trim();
switch (key) {
case "Digest qop":
authInfo.authType = SHELLY2_AUTHTTYPE_DIGEST;
break;
case "realm":
authInfo.realm = value;
break;
case "nonce":
// authInfo.nonce = Long.parseLong(value, 16);
authInfo.nonce = value;
break;
case "algorithm":
authInfo.algorithm = value;
break;
}
}
json = httpPost(authInfo, gson.toJson(req));
} else {
throw e;
}
}
Shelly2RpcBaseMessage response = gson.fromJson(json, Shelly2RpcBaseMessage.class);
if (response == null) {
throw new ShellyApiException("Unable to convert API result to object");
}
if (response.result != null) {
// return sub element result as requested class type
json = gson.toJson(response.result);
boolean isString = response.result instanceof String;
return fromJson(gson, isString && "null".equalsIgnoreCase(((String) response.result)) ? "{}" : json,
classOfT);
} else {
// return direct format
@Nullable
T result = gson.fromJson(json, classOfT == String.class ? Shelly2RpcBaseMessage.class : classOfT);
if (result == null) {
throw new ShellyApiException("Unable to convert API result to object");
}
return result;
}
}
@Override
public void close() {
}
protected @Nullable ArrayList<@Nullable ShellySettingsRelay> fillRelaySettings(ShellyDeviceProfile profile,
Shelly2GetConfigResult dc) {
ArrayList<@Nullable ShellySettingsRelay> relays = new ArrayList<>();
@@ -938,6 +1212,16 @@ public class Shelly2ApiClient extends ShellyHttpClient {
}
}
protected void fillWiFiSta(@Nullable Shelly2DeviceConfigSta from, ShellySettingsWiFiNetwork to) {
to.enabled = from != null && !getString(from.ssid).isEmpty();
if (from != null) {
to.ssid = from.ssid;
to.ip = from.ip;
to.mask = from.netmask;
to.dns = from.nameserver;
}
}
protected @Nullable ArrayList<@Nullable ShellySettingsInput> fillInputSettings(ShellyDeviceProfile profile,
Shelly2GetConfigResult dc) {
if (dc.input0 == null) {
@@ -20,6 +20,7 @@ import static org.openhab.binding.shelly.internal.api2.ShellyBluJsonDTO.SHELLY2_
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -31,9 +32,10 @@ import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
@@ -65,11 +67,9 @@ import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2APClien
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthChallenge;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2ConfigParms;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DevConfigBle.Shelly2DevConfigBleObserver;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DeviceConfigSta;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp.Shelly2DeviceConfigApRE;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceSettings;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusLight;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2RGBWStatus;
@@ -87,7 +87,6 @@ import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptLi
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse.ShellyScriptListEntry;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptPutCodeParams;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptResponse;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
@@ -109,9 +108,9 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
private final ShellyThingTable thingTable;
protected boolean initialized = false;
private boolean discovery = false;
private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
private @Nullable Shelly2RpcSocket rpcSocket;
private @Nullable Shelly2AuthChallenge authInfo;
private final WebSocketClient client;
// Plus devices support up to 3 scripts, Pro devices up to 10
// We need to find a free script id when uploading our script
@@ -124,25 +123,11 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
* @param thingName Symbolic thing name
* @param thing Thing Handler (ThingHandlerInterface)
*/
public Shelly2ApiRpc(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
public Shelly2ApiRpc(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing,
WebSocketClient webSocketClient) {
super(thingName, thing);
this.thingName = thingName;
this.thing = thing;
this.thingTable = thingTable;
}
/**
* Simple initialization - called by discovery handler
*
* @param thingName Symbolic thing name
* @param config Thing Configuration
* @param httpClient HTTP Client to be passed to ShellyHttpClient
*/
public Shelly2ApiRpc(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
super(thingName, config, httpClient);
this.thingName = thingName;
this.thingTable = new ShellyThingTable(); // create empty table;
this.discovery = true;
this.client = webSocketClient;
}
@Override
@@ -151,8 +136,13 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
logger.debug("{}: Disconnect Rpc Socket on initialize", thingName);
disconnect();
}
rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
Shelly2RpcSocket rpcSocket = this.rpcSocket;
if (rpcSocket != null) {
rpcSocket.disconnect();
}
rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp, client);
rpcSocket.addMessageHandler(this);
this.rpcSocket = rpcSocket;
initialized = true;
}
@@ -342,57 +332,45 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
}
profile.initialized = true;
if (!discovery) {
getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
try {
if (profile.alwaysOn && dc.ble != null) {
logger.debug("{}: BLU Gateway support is {} for this device", thingName,
config.enableBluGateway ? "enabled" : "disabled");
if (config.enableBluGateway) {
boolean bluetooth = getBool(dc.ble.enable);
boolean observer = dc.ble.observer != null && getBool(dc.ble.observer.enable);
if (!bluetooth) {
logger.warn("{}: Bluetooth will be enabled to activate BLU Gateway mode", thingName);
}
if (observer) {
logger.warn("{}: Shelly Cloud Bluetooth Gateway conflicts with openHAB, disabling it",
thingName);
}
boolean restart = false;
if (!bluetooth || observer) {
logger.info("{}: Setup openHAB BLU Gateway", thingName);
restart = setBluetooth(true);
}
try {
if (profile.alwaysOn && dc.ble != null) {
logger.debug("{}: BLU Gateway support is {} for this device", thingName,
config.enableBluGateway ? "enabled" : "disabled");
if (config.enableBluGateway) {
boolean bluetooth = getBool(dc.ble.enable);
boolean observer = dc.ble.observer != null && getBool(dc.ble.observer.enable);
if (!bluetooth) {
logger.warn("{}: Bluetooth will be enabled to activate BLU Gateway mode", thingName);
}
if (observer) {
logger.warn("{}: Shelly Cloud Bluetooth Gateway conflicts with openHAB, disabling it",
thingName);
}
boolean restart = false;
if (!bluetooth || observer) {
logger.info("{}: Setup openHAB BLU Gateway", thingName);
restart = setBluetooth(true);
}
installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway && bluetooth);
installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway && bluetooth);
if (restart) {
logger.info("{}: Restart device to activate BLU Gateway", thingName);
deviceReboot();
getThing().reinitializeThing();
}
if (restart) {
logger.info("{}: Restart device to activate BLU Gateway", thingName);
deviceReboot();
getThing().reinitializeThing();
}
}
} catch (ShellyApiException e) {
logger.debug("{}: Device config failed", thingName, e);
}
} catch (ShellyApiException e) {
logger.debug("{}: Device config failed", thingName, e);
}
return profile;
}
private void fillWiFiSta(@Nullable Shelly2DeviceConfigSta from, ShellySettingsWiFiNetwork to) {
to.enabled = from != null && !getString(from.ssid).isEmpty();
if (from != null) {
to.ssid = from.ssid;
to.ip = from.ip;
to.mask = from.netmask;
to.dns = from.nameserver;
}
}
private void checkSetWsCallback() throws ShellyApiException {
Shelly2ConfigParms wsConfig = apiRequest(SHELLYRPC_METHOD_WSGETCONFIG, null, Shelly2ConfigParms.class);
String url = "ws://" + config.localIp + ":" + config.localPort + "/shelly/wsevent";
@@ -606,7 +584,6 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
@Override
public void onConnect(String deviceIp, boolean connected) {
ShellyThingTable thingTable = this.thingTable;
thing = thingTable.getThing(deviceIp);
logger.debug("{}: Get thing from thingTable", thingName);
}
@@ -616,8 +593,8 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
logger.debug("{}: NotifyStatus update received: {}", thingName, gson.toJson(message));
ShellyThingInterface t = thing;
if (t == null) {
logger.debug("{}: No matching thing on NotifyStatus for {}, ignore (src={}, dst={}, discovery={})",
thingName, thingName, message.src, message.dst, discovery);
logger.debug("{}: No matching thing on NotifyStatus for {}, ignore (src={}, dst={})", thingName, thingName,
message.src, message.dst);
return;
}
if (t.isStopping()) {
@@ -773,7 +750,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
logger.debug("{}: WebSocket connection closed, status = {}/{}", thingName, statusCode, reason);
if ("Bye".equalsIgnoreCase(reason)) {
logger.debug("{}: Device went to sleep mode or was restarted", thingName);
} else if (statusCode == StatusCode.ABNORMAL && !discovery && getProfile().alwaysOn) {
} else if (statusCode == StatusCode.ABNORMAL && getProfile().alwaysOn) {
// e.g. device rebooted
if (getThing().getThingStatusDetail() != ThingStatusDetail.DUTY_CYCLE) {
thingOffline("WebSocket connection closed abnormally");
@@ -787,7 +764,13 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
@Override
public void onError(Throwable cause) {
logger.debug("{}: WebSocket error", thingName, cause);
if (logger.isDebugEnabled()) {
if (cause instanceof EofException || cause instanceof EOFException) {
logger.debug("{}: WebSocket was closed ungracefully", thingName);
} else {
logger.debug("{}: WebSocket error", thingName, cause);
}
}
ShellyThingInterface thing = this.thing;
if (thing != null && thing.getProfile().alwaysOn) {
thingOffline("WebSocket error");
@@ -802,21 +785,6 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
}
}
@Override
public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
Shelly2DeviceSettings device = callApi("/shelly", Shelly2DeviceSettings.class);
ShellySettingsDevice info = new ShellySettingsDevice();
info.hostname = getString(device.id);
info.name = getString(device.name);
info.fw = getString(device.fw);
info.type = getString(device.model);
info.mac = getString(device.mac);
info.auth = getBool(device.auth);
info.gen = getInteger(device.gen);
info.mode = mapValue(MAP_PROFILE, getString(device.profile));
return info;
}
@Override
public ShellySettingsStatus getStatus() throws ShellyApiException {
ShellyDeviceProfile profile = getProfile();
@@ -1277,9 +1245,15 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
private void asyncApiRequest(String method) throws ShellyApiException {
Shelly2RpcBaseMessage request = buildRequest(method, null);
reconnect();
rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
Shelly2RpcSocket rpcSocket = this.rpcSocket;
if (rpcSocket != null) {
rpcSocket.sendMessage(gson.toJson(request)); // submit, result will be async
} else {
throw new ShellyApiException("RPC socket isn't connected, cannot send async request");
}
}
@Override
public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
String json = "";
Shelly2RpcBaseMessage req = buildRequest(method, params);
@@ -1350,13 +1324,22 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
}
private void reconnect() throws ShellyApiException {
if (!rpcSocket.isConnected()) {
logger.debug("{}: Connect Rpc Socket (discovery = {})", thingName, discovery);
rpcSocket.connect();
Shelly2RpcSocket rpcSocket = this.rpcSocket;
if (rpcSocket != null) {
if (!rpcSocket.isConnected()) {
logger.debug("{}: Connect RPC Socket", thingName);
rpcSocket.connect();
}
} else {
throw new ShellyApiException("RPC socket is not connected");
}
}
private void disconnect() {
Shelly2RpcSocket rpcSocket = this.rpcSocket;
if (rpcSocket == null) {
return;
}
if (rpcSocket.isConnected()) {
logger.trace("{}: Disconnect Rpc Socket", thingName);
}
@@ -1369,9 +1352,15 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
@Override
public void close() {
Shelly2RpcSocket rpcSocket = this.rpcSocket;
if (rpcSocket == null) {
logger.debug("{}: Cannot close RPC socket since it's null", thingName);
initialized = false;
return;
}
if (initialized || rpcSocket.isConnected()) {
logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
logger.debug("{}: Closing Rpc API (socket is {})", thingName,
rpcSocket.isConnected() ? "connected" : "disconnected");
}
disconnect();
initialized = false;
@@ -0,0 +1,135 @@
/*
* Copyright (c) 2010-2026 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.api2;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.IOException;
import java.util.Locale;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.openhab.core.io.net.http.WebSocketFactory;
import org.osgi.service.component.ComponentException;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link Shelly2EventServlet} implements the WebSocket callback for Gen2 devices
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@WebServlet(name = "Shelly2EventServlet", urlPatterns = { SHELLY2_CALLBACK_URI })
@Component(service = HttpServlet.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
public class Shelly2EventServlet extends WebSocketServlet {
private static final long serialVersionUID = -1210354558091063207L;
private final Logger logger = LoggerFactory.getLogger(Shelly2EventServlet.class);
private final ShellyThingTable thingTable;
private final WebSocketClient webSocketClient;
@Activate
public Shelly2EventServlet(@Reference ShellyThingTable thingTable, @Reference WebSocketFactory webSocketFactory) {
this.thingTable = thingTable;
WebSocketClient client = Shelly2RpcSocket.createWebSocketClient(webSocketFactory, "shelly2servlet");
this.webSocketClient = client;
try {
client.start();
logger.debug("Shelly2EventServlet started at {}", SHELLY2_CALLBACK_URI);
} catch (Exception e) {
logger.warn("Failed to start servlet WebSocket client: {}", e.getMessage(), e);
throw new ComponentException("Failed to activate: Unable to start WebSocket client: " + e.getMessage(), e);
}
}
@Deactivate
protected void deactivate() {
logger.debug("Shelly2EventServlet: Stopping");
try {
webSocketClient.stop();
} catch (Exception e) {
logger.warn("Failed to stop servlet WebSocket client: {}", e.getMessage(), e);
}
}
/**
* Servlet handler. WebSocket call.
*/
@Override
protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse resp)
throws ServletException, IOException, IllegalArgumentException {
if (request == null) {
logger.trace("Shelly2EventServlet.service unexpectedly received a null request. Request not processed");
return;
}
String path = getString(request.getRequestURI()).toLowerCase(Locale.ROOT);
if (!path.equals(SHELLY2_CALLBACK_URI)) {
logger.warn("Shelly2EventServlet received unknown request: path = {}", path);
return;
}
if (resp != null) {
super.service(request, resp);
}
}
/**
* WebSocket: register Shelly2RpcSocket class
*/
@Override
public void configure(@Nullable WebSocketServletFactory factory) {
if (factory != null) {
factory.getPolicy().setIdleTimeout(SHELLY_API_TIMEOUT_MS);
factory.setCreator(new Shelly2WebSocketCreator(thingTable, webSocketClient));
factory.register(Shelly2RpcSocket.class);
}
}
public static class Shelly2WebSocketCreator implements WebSocketCreator {
private final Logger logger = LoggerFactory.getLogger(Shelly2WebSocketCreator.class);
private final ShellyThingTable thingTable;
private final WebSocketClient webSocketClient;
public Shelly2WebSocketCreator(ShellyThingTable thingTable, WebSocketClient webSocketClient) {
this.thingTable = thingTable;
this.webSocketClient = webSocketClient;
}
@Override
public Object createWebSocket(@Nullable ServletUpgradeRequest req, @Nullable ServletUpgradeResponse resp) {
logger.debug("WebSocket: Create socket from servlet");
return new Shelly2RpcSocket(thingTable, true, webSocketClient);
}
}
}
@@ -12,6 +12,7 @@
*/
package org.openhab.binding.shelly.internal.api2;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS;
import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.api2.ShellyBluJsonDTO.*;
import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.addBluThing;
@@ -19,14 +20,18 @@ import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.core.HttpHeaders;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
@@ -41,6 +46,7 @@ import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNoti
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.openhab.core.io.net.http.WebSocketFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,23 +57,25 @@ import com.google.gson.Gson;
*/
@NonNullByDefault
@WebSocket(maxIdleTime = Integer.MAX_VALUE)
public class Shelly2RpcSocket {
public class Shelly2RpcSocket implements WriteCallback {
private final Logger logger = LoggerFactory.getLogger(Shelly2RpcSocket.class);
private final Gson gson = new Gson();
private String thingName = "";
private String deviceIp = "";
private boolean inbound = false;
private CountDownLatch connectLatch = new CountDownLatch(1);
private @Nullable Session session;
private @Nullable Shelly2RpctInterface websocketHandler;
private WebSocketClient client = new WebSocketClient();
private volatile String thingName = "";
private volatile String deviceIp = "";
private final boolean inbound;
private final ShellyThingTable thingTable;
public Shelly2RpcSocket() {
thingTable = new ShellyThingTable();
}
// All access must be guarded by "this"
private @Nullable Session session;
// All access must be guarded by "this"
private final List<String> sendQueue = new ArrayList<>();
// All access must be guarded by "this"
private @Nullable Shelly2RpctInterface websocketHandler;
private final WebSocketClient client;
/**
* Regular constructor for Thing and Discover handler
@@ -76,10 +84,13 @@ public class Shelly2RpcSocket {
* @param thingTable
* @param deviceIp IP address for the device
*/
public Shelly2RpcSocket(String thingName, ShellyThingTable thingTable, String deviceIp) {
public Shelly2RpcSocket(String thingName, ShellyThingTable thingTable, String deviceIp,
WebSocketClient webSocketClient) {
this.thingName = thingName;
this.deviceIp = deviceIp;
this.thingTable = thingTable;
this.client = webSocketClient;
inbound = false;
}
/**
@@ -88,9 +99,10 @@ public class Shelly2RpcSocket {
* @param thingTable
* @param inbound
*/
public Shelly2RpcSocket(ShellyThingTable thingTable, boolean inbound) {
public Shelly2RpcSocket(ShellyThingTable thingTable, boolean inbound, WebSocketClient webSocketClient) {
this.thingTable = thingTable;
this.inbound = inbound;
this.client = webSocketClient;
}
/**
@@ -98,142 +110,207 @@ public class Shelly2RpcSocket {
*
* @param interfacehandler
*/
public void addMessageHandler(Shelly2RpctInterface interfacehandler) {
public synchronized void addMessageHandler(Shelly2RpctInterface interfacehandler) {
this.websocketHandler = interfacehandler;
}
/**
* Connect outbound Web Socket
* Connect outbound Web Socket.
*
* @throws ShellyApiException
* NOTE: sendQueue is NOT preserved across reconnects; it is cleared on any disconnect/close/error.
*/
public void connect() throws ShellyApiException {
String deviceIp = this.deviceIp;
if (deviceIp.isBlank()) {
throw new ShellyApiException(thingName + ": Device IP not set");
}
// Prepare connect
URI uri;
try {
uri = new URI("ws://" + deviceIp + SHELLYRPC_ENDPOINT);
} catch (URISyntaxException e) {
throw new ShellyApiException("Invalid URI: " + e.getMessage(), e);
}
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader(HttpHeaders.HOST, deviceIp);
request.setHeader("Origin", "http://" + deviceIp);
request.setHeader("Pragma", "no-cache");
request.setHeader("Cache-Control", "no-cache");
if (logger.isTraceEnabled()) {
logger.trace("{}: Connect WebSocket, URI={}", thingName, uri);
}
// Start connecting the WebSocket session (result will be passed to onConnect()/onError())
synchronized (this) {
disconnect(); // for safety
URI uri = new URI("ws://" + deviceIp + SHELLYRPC_ENDPOINT);
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader(HttpHeaders.HOST, deviceIp);
request.setHeader("Origin", "http://" + deviceIp);
request.setHeader("Pragma", "no-cache");
request.setHeader("Cache-Control", "no-cache");
logger.trace("{}: Connect WebSocket, URI={}", thingName, uri);
client = new WebSocketClient();
connectLatch = new CountDownLatch(1);
client.start();
client.setConnectTimeout(5000);
client.setStopTimeout(0);
client.connect(this, uri, request);
} catch (Exception e) {
throw new ShellyApiException("Unable to initialize WebSocket", e);
// Connect async
try {
client.connect(this, uri, request);
} catch (RuntimeException | IOException e) {
// Keep this if your Jetty version still declares IOException on start()/connect path
throw new ShellyApiException("Failed to connect WebSocket: " + e.getMessage(), e);
}
}
}
/**
* Web Socket is connected, lookup thing and create connectLatch to synchronize first sendMessage()
* Web Socket is connected, get thing handler and send already queued messages.
*
* @param session Newly created WebSocket connection
*/
@OnWebSocketConnect
public void onConnect(Session session) {
Shelly2RpctInterface handler = websocketHandler;
try {
if (session.getRemoteAddress() == null) {
if (session.getRemoteAddress() == null) {
if (logger.isDebugEnabled()) {
logger.debug("{}: Invalid inbound WebSocket connect (invalid remote ip)", thingName);
session.close(StatusCode.ABNORMAL, "Invalid remote IP");
return;
}
this.session = session;
if (deviceIp.isEmpty()) {
// This is the inbound event web socket
deviceIp = session.getRemoteAddress().getAddress().getHostAddress();
}
session.close(StatusCode.ABNORMAL, "Invalid remote IP");
return;
}
String deviceIp = this.deviceIp;
if (deviceIp.isEmpty()) {
// This is the inbound event web socket
this.deviceIp = deviceIp = session.getRemoteAddress().getAddress().getHostAddress();
}
// It's a bit wasteful to retrieve the ThingInterface even if we already have a handler, but we can't call
// thingTable.getThing() while holding a lock safely, which is why it's done "in case it's needed" before
// the lock is acquired.
ShellyThingInterface thing;
try {
thing = thingTable.getThing(deviceIp);
} catch (IllegalArgumentException e) { // unknown thing
logger.debug("{}: RPC connection error for {} (unknown/disabled thing? - {}), closing socket", thingName,
deviceIp, e.getMessage());
session.close(StatusCode.SHUTDOWN, "Thing not active");
return;
}
Shelly2RpctInterface handler;
List<String> queue = null;
synchronized (this) {
handler = websocketHandler;
this.session = session;
if (handler == null) {
ShellyThingInterface thing = thingTable.getThing(deviceIp);
Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
handler = api.getRpcHandler();
websocketHandler = handler;
}
connectLatch.countDown();
if (!sendQueue.isEmpty()) {
queue = List.copyOf(sendQueue);
sendQueue.clear();
}
}
if (logger.isDebugEnabled()) {
logger.debug("{}: WebSocket connected {}<-{}, Idle Timeout={}", thingName, session.getLocalAddress(),
session.getRemoteAddress(), session.getIdleTimeout());
handler.onConnect(deviceIp, true);
} catch (IllegalArgumentException e) { // unknown thing
if (handler == null) { // we had a session already
logger.debug("{}:RPC Connection error for {} (unknown/disabled thing? - {}), closing socket", thingName,
deviceIp, e.getMessage());
}
handler.onConnect(deviceIp, true);
if (queue != null) {
if (logger.isDebugEnabled()) {
logger.debug("{}: Sending {} queued RPC request{}", thingName, queue.size(),
queue.size() > 1 ? "s" : "");
}
RemoteEndpoint remote = session.getRemote();
for (String msg : queue) {
remote.sendString(msg, this);
}
session.close(StatusCode.SHUTDOWN, "Thing not active");
}
}
/**
* Send request over WebSocket
* Send request over WebSocket.
* Queueing policy (simple):
* - If not connected: enqueue and return.
* - If connected: flush queued snapshot (if any), then send this message.
* - If send fails: this is an error; do NOT requeue.
*
* @param str API request message
* @throws ShellyApiException
*/
public void sendMessage(String str) throws ShellyApiException {
Session session = this.session;
if (session != null) {
try {
connectLatch.await();
session.getRemote().sendString(str);
public void sendMessage(String str) {
Session session;
List<String> queue = null;
synchronized (this) {
session = this.session;
if (session == null || !session.isOpen()) {
this.sendQueue.add(str);
if (logger.isTraceEnabled()) {
logger.trace("{}: Queued RPC request (no open session): {}", thingName, str);
} else if (logger.isDebugEnabled()) {
logger.debug("{}: Queued RPC request (no open session)", thingName);
}
return;
} catch (IOException | InterruptedException e) {
throw new ShellyApiException("Error RpcSend failed", e);
}
if (!this.sendQueue.isEmpty()) {
queue = List.copyOf(this.sendQueue);
this.sendQueue.clear();
}
}
throw new ShellyApiException("Unable to send API request (No Rpc session)");
RemoteEndpoint remote = session.getRemote();
if (queue != null) {
if (logger.isDebugEnabled()) {
logger.debug("{}: Sending {} queued API request{}", thingName, queue.size(),
queue.size() > 1 ? "s" : "");
}
for (String queued : queue) {
remote.sendString(queued, this);
}
}
if (logger.isTraceEnabled()) {
logger.trace("{}: Sending RPC message {}", thingName, str);
}
remote.sendString(str, this);
}
/**
* Close WebSocket session
* Close WebSocket session and clean up.
* <p>
* Clears {@code sendQueue} (NOT preserved across reconnects).
*/
public void disconnect() {
try {
Session session = this.session;
if (session != null) {
if (session.isOpen()) {
logger.trace("{}: Disconnecting WebSocket ({} -> {})", thingName, session.getLocalAddress(),
session.getRemoteAddress());
}
session.disconnect();
session.close(StatusCode.NORMAL, "Socket closed");
this.session = null;
}
} catch (Exception e) {
if (e.getCause() instanceof InterruptedException) {
logger.debug("{}: Unable to close socket - interrupted", thingName); // e.g. device was rebooted
} else {
logger.debug("{}: Unable to close socket", thingName, e);
}
} finally {
// make sure client is stopped / thread terminates / socket resource is free up
try {
client.stop();
} catch (Exception e) {
logger.debug("{}: Unable to close Web Socket", thingName, e);
Session session;
synchronized (this) {
session = this.session;
cleanup();// set session=null, clear send queue
}
if (session != null && session.isOpen()) {
if (logger.isTraceEnabled()) {
logger.trace("{}: Closing WebSocket session ({} -> {})", thingName, session.getLocalAddress(),
session.getRemoteAddress());
}
session.close(StatusCode.NORMAL, "Socket closed");
}
}
/**
* Inbound WebSocket message
* Process Inbound WebSocket message
*
* @param session WebSocket session
* @param receivedMessage Textial API message
* @param receivedMessage Textual API message
*/
@OnWebSocketMessage
public void onText(Session session, String receivedMessage) {
Shelly2RpctInterface handler = websocketHandler;
Shelly2RpctInterface handler;
synchronized (this) {
handler = websocketHandler;
}
try {
Shelly2RpcBaseMessage message = fromJson(gson, receivedMessage, Shelly2RpcBaseMessage.class);
logger.trace("{}: Inbound Rpc message: {}", thingName, receivedMessage);
if (logger.isTraceEnabled()) {
logger.trace("{}: Inbound RPC message: {}", thingName, receivedMessage);
}
if (handler != null) {
if (thingName.isEmpty()) {
thingName = getString(message.src);
@@ -259,7 +336,6 @@ public class Shelly2RpcSocket {
for (Shelly2NotifyEvent e : events.params.events) {
if (getString(e.event).startsWith(SHELLY2_EVENT_BLUPREFIX)) {
String address = getString(e.blu != null ? e.blu.addr : "").replace(":", "");
ShellyThingTable thingTable = this.thingTable;
if (thingTable.findThing(address) != null) {
// known device
ShellyThingInterface thing = thingTable.getThing(address);
@@ -286,15 +362,17 @@ public class Shelly2RpcSocket {
handler.onMessage(receivedMessage);
}
} else {
logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
getString(message.src), receivedMessage);
if (logger.isDebugEnabled()) {
logger.debug("{}: No RPC listener registered for device {}, skip message: {}", thingName,
getString(message.src), receivedMessage);
}
}
} catch (ShellyApiException | IllegalArgumentException e) {
logger.debug("{}: Unable to process Rpc message ({}): {}", thingName, e.getMessage(), receivedMessage);
}
}
public boolean isConnected() {
public synchronized boolean isConnected() {
Session session = this.session;
return session != null && session.isOpen();
}
@@ -304,41 +382,111 @@ public class Shelly2RpcSocket {
}
/**
* Web Socket closed, notify thing handler
* WebSocket closed, notify thing handler (close initiated by the binding)
*
* @param statusCode StatusCode
* @param reason Textual reason
*/
@OnWebSocketClose
public void onClose(int statusCode, String reason) {
if (statusCode != StatusCode.NORMAL) {
logger.trace("{}: Rpc connection closed: {} - {}", thingName, statusCode, getString(reason));
if (statusCode != StatusCode.NORMAL && logger.isTraceEnabled()) {
logger.trace("{}: RPC connection closed abnormally: {} - {}", thingName, statusCode, getString(reason));
}
Shelly2RpctInterface handler;
synchronized (this) {
handler = this.websocketHandler;
// set session=null, clear send queue
// this also prevents another socket closed issued by thingOffline()->api-close()->close()
cleanup();
}
if (inbound) {
// Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
return;
}
disconnect();
Shelly2RpctInterface websocketHandler = this.websocketHandler;
if (websocketHandler != null) {
websocketHandler.onClose(statusCode, reason);
if (handler != null) {
handler.onClose(statusCode, reason);
}
}
/**
* WebSocket error handler
* Callback for WebSocket error (disconnect initiated by remote).
*
* @param cause WebSocket error/Exception
*/
@OnWebSocketError
public void onError(Throwable cause) {
Shelly2RpctInterface websocketHandler;
synchronized (this) {
websocketHandler = this.websocketHandler;
// set session=null, clear send queue
// this also prevents another socket closed issued by thingOffline()->api-close()->close()
cleanup();
}
if (inbound) {
// Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
return;
}
Shelly2RpctInterface websocketHandler = this.websocketHandler;
if (websocketHandler != null) {
websocketHandler.onError(cause);
}
}
/**
* Clears session and drops queued messages.
* Must only be called when session has been/is being closed one way or another.
*/
private void cleanup() {
int qLength;
synchronized (this) {
this.session = null;
qLength = sendQueue.size();
sendQueue.clear();
}
if (logger.isDebugEnabled() && qLength > 0) {
logger.debug("{}: {} queued RPC message{} dropped, because the socket was closed", thingName, qLength,
qLength != 1 ? "s were" : " was");
}
}
/**
* Asynchronous write completed with error
*/
@Override
public void writeFailed(@Nullable Throwable x) {
if (logger.isDebugEnabled()) {
if (x == null) {
logger.debug("{}: Sending RPC Message failed", thingName);
} else {
logger.debug("{}: Sending RPC Message failed: {}", thingName, x.getMessage(), x);
}
}
}
/**
* Asynchronous write completed with success
*/
@Override
public void writeSuccess() {
// Nothing to do
}
/**
* Create and configures a new {@link WebSocketClient}.
*
* @param webSocketFactory the {@link WebSocketFactory} to use to create the new client instance.
* @param consumerName the name for identifying the consumer in the Jetty thread pool.
* Must be between 4 and 20 characters long and must contain only the following characters [a-zA-Z0-9-_]
*/
public static WebSocketClient createWebSocketClient(WebSocketFactory webSocketFactory, String consumerName) {
WebSocketClient client = webSocketFactory.createWebSocketClient(consumerName);
client.setConnectTimeout(SHELLY_API_TIMEOUT_MS);
client.setStopTimeout(1000);
return client;
}
}
@@ -19,6 +19,7 @@ import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
@@ -66,8 +67,9 @@ public class ShellyBluApi extends Shelly2ApiRpc {
* @param thingTable Table of known things (build at runtime)
* @param thing Thing Handler (ThingHandlerInterface)
*/
public ShellyBluApi(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
super(thingName, thingTable, thing);
public ShellyBluApi(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing,
WebSocketClient webSocketClient) {
super(thingName, thingTable, thing, webSocketClient);
ShellyDeviceProfile profile = thing.getProfile();
ThingTypeUID uid = thing.getThing().getThingTypeUID();
@@ -26,12 +26,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyDiscoveryInterface;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiClient;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
@@ -111,12 +111,13 @@ public class ShellyBasicDiscoveryService extends AbstractDiscoveryService {
}
public static @Nullable DiscoveryResult createResult(boolean gen2, String hostname, String ipAddress,
ShellyBindingConfiguration bindingConfig, HttpClient httpClient, ShellyTranslationProvider messages) {
ShellyBindingConfiguration bindingConfig, HttpClient httpClient, ShellyTranslationProvider messages,
ShellyThingTable thingTable) {
Logger logger = LoggerFactory.getLogger(ShellyBasicDiscoveryService.class);
ThingUID thingUID = null;
ShellyDeviceProfile profile;
ShellySettingsDevice devInfo;
ShellyApiInterface api = null;
ShellyDiscoveryInterface api = null;
boolean auth = false;
String mac = "";
String model = "";
@@ -128,7 +129,11 @@ public class ShellyBasicDiscoveryService extends AbstractDiscoveryService {
try {
ShellyThingConfiguration config = fillConfig(bindingConfig, ipAddress);
api = gen2 ? new Shelly2ApiRpc(name, config, httpClient) : new Shelly1HttpApi(name, config, httpClient);
if (gen2) {
api = new Shelly2ApiClient(name, config, httpClient);
} else {
api = new Shelly1HttpApi(name, config, httpClient);
}
api.initialize();
devInfo = api.getDeviceInfo();
mac = getString(devInfo.mac);
@@ -29,6 +29,7 @@ import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
@@ -66,6 +67,7 @@ public class ShellyMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant
private final Logger logger = LoggerFactory.getLogger(ShellyMDNSDiscoveryParticipant.class);
private final ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
private final ShellyTranslationProvider messages;
private final ShellyThingTable thingTable;
private final HttpClient httpClient;
private final ConfigurationAdmin configurationAdmin;
@@ -79,11 +81,13 @@ public class ShellyMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant
@Activate
public ShellyMDNSDiscoveryParticipant(@Reference ConfigurationAdmin configurationAdmin,
@Reference HttpClientFactory httpClientFactory, @Reference LocaleProvider localeProvider,
@Reference ShellyTranslationProvider translationProvider, ComponentContext componentContext) {
@Reference ShellyTranslationProvider translationProvider, @Reference ShellyThingTable thingTable,
ComponentContext componentContext) {
logger.debug("Activating Shelly mDNS discovery service");
this.configurationAdmin = configurationAdmin;
this.messages = translationProvider;
this.httpClient = httpClientFactory.getCommonHttpClient();
this.thingTable = thingTable;
bindingConfig.updateFromProperties(componentContext.getProperties());
}
@@ -143,7 +147,7 @@ public class ShellyMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant
boolean gen2 = "2".equals(gen) || "3".equals(gen) || "4".equals(gen)
|| ShellyDeviceProfile.isGeneration2(serviceName);
return ShellyBasicDiscoveryService.createResult(gen2, serviceName, address, bindingConfig, httpClient,
messages);
messages, thingTable);
} catch (IOException e) {
logger.debug("{}: Exception on processing serviceInfo '{}'", serviceName, service.getNiceTextString(), e);
return null;
@@ -30,6 +30,7 @@ import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
@@ -141,7 +142,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
*/
public ShellyBaseHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, ShellyThingTable thingTable,
final Shelly1CoapServer coapServer, final HttpClient httpClient) {
final Shelly1CoapServer coapServer, final HttpClient httpClient, WebSocketClient webSocketClient) {
super(thing);
this.thingTable = thingTable;
@@ -158,9 +159,9 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
blu = ShellyDeviceProfile.isBluSeries(thingTypeUID);
gen2 = ShellyDeviceProfile.isGeneration2(thingTypeUID);
if (blu) {
this.api = new ShellyBluApi(thingName, thingTable, this);
this.api = new ShellyBluApi(thingName, thingTable, this, webSocketClient);
} else if (gen2) {
this.api = new Shelly2ApiRpc(thingName, thingTable, this);
this.api = new Shelly2ApiRpc(thingName, thingTable, this, webSocketClient);
} else {
this.api = new Shelly1HttpApi(thingName, this);
}
@@ -610,7 +611,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
String secondaryIp = config.deviceIp + ":" + client.mport.toString();
String name = SERVICE_NAME_SHELLYPLUSRANGE_PREFIX + "-" + client.mac.replaceAll(":", "");
DiscoveryResult result = ShellyBasicDiscoveryService.createResult(true, name, secondaryIp,
bindingConfig, httpClient, messages);
bindingConfig, httpClient, messages, thingTable);
if (result != null) {
thingTable.discoveredResult(result);
}
@@ -14,6 +14,7 @@ package org.openhab.binding.shelly.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
@@ -32,8 +33,8 @@ public class ShellyBluHandler extends ShellyBaseHandler {
public ShellyBluHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, final ShellyThingTable thingTable,
final Shelly1CoapServer coapServer, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, thingTable, coapServer, httpClient);
final Shelly1CoapServer coapServer, final HttpClient httpClient, WebSocketClient webSocketClient) {
super(thing, translationProvider, bindingConfig, thingTable, coapServer, httpClient, webSocketClient);
}
@Override
@@ -22,6 +22,7 @@ import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRgbwLight;
@@ -60,8 +61,8 @@ public class ShellyLightHandler extends ShellyBaseHandler {
public ShellyLightHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, final ShellyThingTable thingTable,
final Shelly1CoapServer coapServer, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, thingTable, coapServer, httpClient);
final Shelly1CoapServer coapServer, final HttpClient httpClient, WebSocketClient webSocketClient) {
super(thing, translationProvider, bindingConfig, thingTable, coapServer, httpClient, webSocketClient);
channelColors = new TreeMap<>();
}
@@ -14,6 +14,7 @@ package org.openhab.binding.shelly.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
@@ -38,8 +39,8 @@ public class ShellyProtectedHandler extends ShellyBaseHandler {
*/
public ShellyProtectedHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, ShellyThingTable thingTable,
final Shelly1CoapServer coapService, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, thingTable, coapService, httpClient);
final Shelly1CoapServer coapService, final HttpClient httpClient, WebSocketClient webSocketClient) {
super(thing, translationProvider, bindingConfig, thingTable, coapService, httpClient, webSocketClient);
}
@Override
@@ -18,6 +18,7 @@ import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
@@ -62,8 +63,8 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
*/
public ShellyRelayHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, ShellyThingTable thingTable,
final Shelly1CoapServer coapServer, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, thingTable, coapServer, httpClient);
final Shelly1CoapServer coapServer, final HttpClient httpClient, WebSocketClient webSocketClient) {
super(thing, translationProvider, bindingConfig, thingTable, coapServer, httpClient, webSocketClient);
}
@Override
@@ -12,8 +12,9 @@
*/
package org.openhab.binding.shelly.internal.handler;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -27,32 +28,39 @@ import org.osgi.service.component.annotations.Deactivate;
/***
* The{@link ShellyThingTable} implements a simple table to allow dispatching incoming events to the proper thing
* handler
* handler.
* <p>
* <b>Note:</b> This class is an OSGi component and is intended to be managed as a singleton by the framework.
* Therefore, do <b>not</b> instantiate it directly via its constructor; always obtain the instance from OSGi.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@Component(service = ShellyThingTable.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
public class ShellyThingTable {
private Map<String, ShellyThingInterface> thingTable = new ConcurrentHashMap<>();
// All access must be guarded by "this"
private final Map<String, ShellyThingInterface> table = new HashMap<>();
// All access must be guarded by "this"
private @Nullable ShellyBasicDiscoveryService discoveryService;
public void addThing(String key, ShellyThingInterface thing) {
if (thingTable.containsKey(key)) {
thingTable.remove(key);
}
thingTable.put(key, thing);
public synchronized @Nullable ShellyThingInterface addThing(String key, ShellyThingInterface thing) {
return table.put(key, thing);
}
public @Nullable ShellyThingInterface findThing(String key) {
ShellyThingInterface t = thingTable.get(key);
if (t != null) {
return t;
List<ShellyThingInterface> values;
synchronized (this) {
ShellyThingInterface result = table.get(key);
if (result != null) {
return result;
}
values = List.copyOf(table.values());
}
for (Map.Entry<String, ShellyThingInterface> entry : thingTable.entrySet()) {
t = entry.getValue();
if (t.checkRepresentation(key)) {
return t;
for (ShellyThingInterface thingInterface : values) {
if (thingInterface.checkRepresentation(key)) {
return thingInterface;
}
}
return null;
@@ -66,52 +74,71 @@ public class ShellyThingTable {
return t;
}
public void removeThing(String key) {
if (thingTable.containsKey(key)) {
thingTable.remove(key);
}
public synchronized @Nullable ShellyThingInterface removeThing(String key) {
return table.remove(key);
}
public Map<String, ShellyThingInterface> getTable() {
return thingTable;
public synchronized Map<String, ShellyThingInterface> getAll() {
return Map.copyOf(table);
}
public int size() {
return thingTable.size();
public synchronized int size() {
return table.size();
}
public void startDiscoveryService(BundleContext bundleContext) {
if (discoveryService == null) {
ShellyBasicDiscoveryService discoveryService = this.discoveryService = new ShellyBasicDiscoveryService(
bundleContext, this);
/**
* Start the discovery service by registering the service with OSGi.
*
* @param bundleContext the {@link BundleContext} to use.
* @throws IllegalStateException If the {@link BundleContext} is no longer valid.
*/
public void startDiscoveryService(BundleContext bundleContext) throws IllegalStateException {
synchronized (this) {
ShellyBasicDiscoveryService discoveryService = this.discoveryService;
if (discoveryService != null) {
return;
}
this.discoveryService = discoveryService = new ShellyBasicDiscoveryService(bundleContext, this);
discoveryService.registerDeviceDiscoveryService();
}
}
public void startScan() {
for (Map.Entry<String, ShellyThingInterface> thing : thingTable.entrySet()) {
(thing.getValue()).startScan();
List<ShellyThingInterface> values;
synchronized (this) {
values = List.copyOf(table.values());
}
for (ShellyThingInterface thingInterface : values) {
thingInterface.startScan();
}
}
public void stopDiscoveryService() {
ShellyBasicDiscoveryService discoveryService = this.discoveryService;
if (discoveryService != null) {
discoveryService.unregisterDeviceDiscoveryService();
synchronized (this) {
ShellyBasicDiscoveryService discoveryService = this.discoveryService;
this.discoveryService = null;
if (discoveryService != null) {
discoveryService.unregisterDeviceDiscoveryService();
}
}
}
public void discoveredResult(ThingTypeUID uid, String model, String serviceName, String address,
Map<String, Object> properties) {
ShellyBasicDiscoveryService discoveryService = this.discoveryService;
ShellyBasicDiscoveryService discoveryService;
synchronized (this) {
discoveryService = this.discoveryService;
}
if (discoveryService != null) {
discoveryService.discoveredResult(uid, model, serviceName, address, properties);
}
}
public void discoveredResult(DiscoveryResult result) {
ShellyBasicDiscoveryService discoveryService = this.discoveryService;
ShellyBasicDiscoveryService discoveryService;
synchronized (this) {
discoveryService = this.discoveryService;
}
if (discoveryService != null) {
discoveryService.discoveredResult(result);
}