mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[shelly] Fix Shelly2RpcSocket concurrency issues (#20069)
Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
This commit is contained in:
+34
-11
@@ -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;
|
||||
|
||||
+1
-13
@@ -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();
|
||||
}
|
||||
|
||||
+35
@@ -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();
|
||||
}
|
||||
+12
-7
@@ -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;
|
||||
|
||||
+14
-63
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+286
-2
@@ -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) {
|
||||
|
||||
+79
-90
@@ -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;
|
||||
|
||||
+135
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+253
-105
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -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();
|
||||
|
||||
+10
-5
@@ -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);
|
||||
|
||||
+6
-2
@@ -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;
|
||||
|
||||
+5
-4
@@ -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);
|
||||
}
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+3
-2
@@ -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<>();
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+61
-34
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user