Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
88056f4
Make Shelly2RpcSocket thread-safe and get rid of CountdownLatch
Jan 12, 2026
21a82a0
Make ShellyThingTable thread-safe
Jan 14, 2026
5397deb
Clean up ShellyHttpClient.thingName a bit, and make ShellyHttpClient.…
Feb 6, 2026
8358aa7
Address some of the review comments
Feb 10, 2026
655bf59
Replace IllegalArgumentException with ShellyApiException
Feb 10, 2026
b84eaf5
Rudimentary refactoring to let WebSocketClient share lifecycle with t…
Feb 11, 2026
4445803
Tweak the "mock" WebSocketFactory to return different client instance…
Feb 11, 2026
7ba65fd
Refactor WebSocketClient lifecycle
Feb 11, 2026
c0de807
Move createWebSocketClient() utility method
Feb 12, 2026
e91989a
Split ShellyEventServlet into Shelly1EventServlet and Shelly2EventSer…
Feb 12, 2026
8147646
Move EventServlet classes
Feb 13, 2026
69ff0aa
Avoid initializing WebSocket during discovery
Feb 13, 2026
ae41992
Remove 'discovery' flag from Shelly2ApiRpc since it's not relevant an…
Feb 13, 2026
4738f45
Suppress EofException when WS connection is broken
Feb 13, 2026
9179c21
Include auth type and algorithm in error message
Feb 14, 2026
38431d6
Fix discovery auth problem
Feb 14, 2026
5bf07a0
Suppress EOFException
Feb 15, 2026
9bb8048
Address review comments
Feb 15, 2026
0382413
Address review comments
Feb 16, 2026
abade27
Integrate fix for "duplicate id" into Nahadar's PR
markus7017 Feb 18, 2026
db335b5
fix placeholders
markus7017 Feb 18, 2026
78e6091
Check also EofException as connection error (beside EOFException)
markus7017 Feb 20, 2026
ea8a1a1
remove WS poll, do async GetStatus only once and only for alwaysOn
markus7017 Feb 20, 2026
091e48f
removed "old" Shely2ApiRc constructor for discovery service (no longer
markus7017 Feb 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
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;
Expand All @@ -33,6 +35,7 @@
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;
Expand All @@ -41,8 +44,10 @@
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;
Expand All @@ -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();

/**
Expand All @@ -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.warn("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;
Expand All @@ -97,11 +111,16 @@ public ShellyHandlerFactory(@Reference NetworkAddressService networkAddressServi
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
Expand All @@ -117,19 +136,23 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
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) {
Expand Down Expand Up @@ -166,7 +189,7 @@ protected synchronized void removeHandler(ThingHandler thingHandler) {
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
Expand All @@ -181,7 +204,7 @@ public ShellyBindingConfiguration getBindingConfig() {

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.io.EofException;

import com.google.gson.JsonSyntaxException;

Expand Down Expand Up @@ -116,7 +117,8 @@ public boolean isConnectionError() {
Class<?> exType = getCauseClass();
return isUnknownHost() || isMalformedURL() || exType == ConnectException.class
|| exType == SocketException.class || exType == PortUnreachableException.class
|| exType == NoRouteToHostException.class || exType == EOFException.class;
|| exType == NoRouteToHostException.class || exType == EofException.class
|| exType == EOFException.class;
}

public boolean isNoRouteToHost() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@
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;
Expand All @@ -27,26 +25,18 @@
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
*
* @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;
Expand Down Expand Up @@ -141,7 +131,5 @@ ShellyDeviceProfile getDeviceProfile(ThingTypeUID thingTypeUID, @Nullable Shelly

void postEvent(String device, String index, String event, Map<String, String> parms) throws ShellyApiException;

void close();

void startScan();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.binding.shelly.internal.config.ShellyThingConfiguration;
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(String thingName, ShellyThingConfiguration config) throws ShellyApiException;

ShellySettingsDevice getDeviceInfo() throws ShellyApiException;

ShellyDeviceProfile getDeviceProfile(ThingTypeUID thingTypeUID, @Nullable ShellySettingsDevice device)
throws ShellyApiException;

void close();
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -233,7 +236,9 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri, @Nullable Sh
}
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;
Expand Down
Loading