Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[shelly] Add support for Range Extender feature #16419

Merged
merged 5 commits into from
Mar 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions bundles/org.openhab.binding.shelly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,27 @@ In this case the binding could directly access the device to retrieve the requir
Otherwise a Thing of type shellyprotected is created in the Inbox and you could set the credentials while adding the Thing.
In this case the credentials are persisted as part of the Thing configuration.

### Range Extender Mode

The Plus/Pro devices support the so-called Range Extender Mode (not available for Gen1).
This allows connect Shellys, which are normally no reachable, because of a lack of WiFi signal.
Once enabled the Shelly acts as a hub to the linked devices, like a WiFi repeater.
The hub device enables the access point, which can be seen by the linked device.
The binding could then get access to the secondary device using <ub shelly ip>:<special port>.
A special port on the hub device will be created for every linked device so one hub device could supported multiple linked devices.


The binding communicates with the Shelly hub device, which then forwards the request to the secondary device.
Once the thing for the primary Shelly goes online the binding detects the enabled range extender mode and adds all connected secondary devices to the Inbox.
This means: The primary Shelly has to complete initialization before linked secondary devices are discovered.

- Discover primary/hub Shelly
- Add thing and wait until it goes ONLINE
- Check Inbox to find the secondary/linked devices
- Add secondary device as usual

If you are adding another secondary device to the same hub device you need to suspend and resume the primary thing, this will run a new initialization and adds the new secondary device to the Inbox.

### Dynamic creation of channels

The Shelly series of devices has many combinations of relays, meters (different versions), sensors etc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2APClientList;
import org.openhab.core.thing.CommonTriggerEvents;

import com.google.gson.annotations.SerializedName;
Expand Down Expand Up @@ -594,6 +595,7 @@ public static class ShellySettingsGlobal {
public Boolean wifiRecoveryReboot; // FW 1.10+
@SerializedName("ap_roaming")
public ShellyApRoaming apRoaming; // FW 1.10+
public Boolean rangeExtender; // Gen2: Range extender
clinique marked this conversation as resolved.
Show resolved Hide resolved

public ShellySettingsMqtt mqtt = new ShellySettingsMqtt();
public ShellySettingsSntp sntp = new ShellySettingsSntp();
Expand Down Expand Up @@ -742,6 +744,7 @@ public static class ShellySettingsStatus {
// /settings/sta for details
public ShellyStatusCloud cloud = new ShellyStatusCloud();
public ShellyStatusMqtt mqtt = new ShellyStatusMqtt();
public Shelly2APClientList rangeExtender;

public String time;
public Integer serial = -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class Shelly2ApiJsonDTO {
public static final String SHELLYRPC_METHOD_LED_SETCONFIG = "WD_UI.SetConfig";
public static final String SHELLYRPC_METHOD_WIFIGETCONG = "Wifi.GetConfig";
public static final String SHELLYRPC_METHOD_WIFISETCONG = "Wifi.SetConfig";
public static final String SHELLYRPC_METHOD_WIFILISTAPCLIENTS = "WiFi.ListAPClients";
public static final String SHELLYRPC_METHOD_ETHGETCONG = "Eth.GetConfig";
public static final String SHELLYRPC_METHOD_ETHSETCONG = "Eth.SetConfig";
public static final String SHELLYRPC_METHOD_BLEGETCONG = "BLE.GetConfig";
Expand Down Expand Up @@ -520,6 +521,21 @@ public class Shelly2DeviceConfigWiFi {
public Shelly2GetConfigResult result;
}

public static class Shelly2APClientList {
public static class Shelly2APClient {
public String mac;
public String ip;
@SerializedName("ip_static")
public Boolean staticIP;
clinique marked this conversation as resolved.
Show resolved Hide resolved
public Integer mport;
clinique marked this conversation as resolved.
Show resolved Hide resolved
public Long since;
clinique marked this conversation as resolved.
Show resolved Hide resolved
}

public Long ts;
clinique marked this conversation as resolved.
Show resolved Hide resolved
@SerializedName("ap_clients")
public ArrayList<Shelly2APClient> apClients;
}

public static class Shelly2DeviceStatus {
public class Shelly2InputCounts {
public Integer total;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2APClientList;
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.Shelly2DeviceConfig.Shelly2DeviceConfigSta;
Expand Down Expand Up @@ -148,7 +149,9 @@ public boolean isInitialized() {
@Override
public void startScan() {
try {
installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway);
if (getProfile().isBlu) {
installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway);
}
} catch (ShellyApiException e) {
}
}
Expand Down Expand Up @@ -222,6 +225,9 @@ public ShellyDeviceProfile getDeviceProfile(String thingType, @Nullable ShellySe
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) {
Expand Down Expand Up @@ -797,6 +803,19 @@ public ShellySettingsStatus getStatus() throws ShellyApiException {
}

fillDeviceStatus(status, ds, false);
if (getBool(profile.settings.rangeExtender)) {
try {
// Get List of AP clients
profile.status.rangeExtender = apiRequest(SHELLYRPC_METHOD_WIFILISTAPCLIENTS, null,
Shelly2APClientList.class);
logger.debug("{}: Range extender is enabled, {} clients connected", thingName,
profile.status.rangeExtender.apClients.size());
} catch (ShellyApiException e) {
logger.debug("{}: Range extender is enabled, but unable to read AP client list", thingName, e);
profile.settings.rangeExtender = false;
}
}

return status;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,14 @@ public class ShellyThingConfiguration {
public String serviceName = "";

public Boolean enableBluGateway = false;
public Boolean enableRangeExtender = true;
clinique marked this conversation as resolved.
Show resolved Hide resolved

@Override
public String toString() {
return "Device address=" + deviceAddress + ", HTTP user/password=" + userId + "/"
+ (password.isEmpty() ? "<none>" : "***") + ", update interval=" + updateInterval + "\n"
+ "Events: Button: " + eventsButton + ", Switch (on/off): " + eventsSwitch + ", Push: " + eventsPush
+ ", Roller: " + eventsRoller + "Sensor: " + eventsSensorReport + ", CoIoT: " + eventsCoIoT + "\n"
+ "Blu Gateway=" + enableBluGateway + ", Range Extender: " + enableRangeExtender;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/**
* Copyright (c) 2010-2024 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.discovery;

import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import static org.openhab.core.thing.Thing.*;

import java.io.IOException;
import java.util.Hashtable;
import java.util.Map;
import java.util.TreeMap;

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.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.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Device discovery creates a thing in the inbox for each vehicle
* found in the data received from {@link ShellyBasicDiscoveryService}.
*
* @author Markus Michels - Initial Contribution
*
*/
@NonNullByDefault
public class ShellyBasicDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(ShellyBasicDiscoveryService.class);

private final BundleContext bundleContext;
private final ShellyThingTable thingTable;
private static final int TIMEOUT = 10;
private @Nullable ServiceRegistration<?> discoveryService;

public ShellyBasicDiscoveryService(BundleContext bundleContext, ShellyThingTable thingTable) {
super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT);
this.bundleContext = bundleContext;
this.thingTable = thingTable;
}

public void registerDeviceDiscoveryService() {
if (discoveryService == null) {
discoveryService = bundleContext.registerService(DiscoveryService.class.getName(), this, new Hashtable<>());
}
}

@Override
protected void startScan() {
logger.debug("Starting BLU Discovery");
thingTable.startScan();
}

public void discoveredResult(ThingTypeUID tuid, String model, String serviceName, String address,
Map<String, Object> properties) {
ThingUID uid = ShellyThingCreator.getThingUID(serviceName, model, "", true);
logger.debug("Adding discovered thing with id {}", uid.toString());
properties.put(PROPERTY_MAC_ADDRESS, address);
String thingLabel = "Shelly BLU " + model + " (" + serviceName + ")";
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
.withRepresentationProperty(PROPERTY_DEV_NAME).withLabel(thingLabel).build();
thingDiscovered(result);
}

public void discoveredResult(DiscoveryResult result) {
thingDiscovered(result);
}

public void unregisterDeviceDiscoveryService() {
if (discoveryService != null) {
discoveryService.unregister();
}
}

@Override
public void deactivate() {
super.deactivate();
unregisterDeviceDiscoveryService();
}

public static @Nullable DiscoveryResult createResult(boolean gen2, String hostname, String ipAddress,
ShellyBindingConfiguration bindingConfig, HttpClient httpClient, ShellyTranslationProvider messages) {
Logger logger = LoggerFactory.getLogger(ShellyBasicDiscoveryService.class);
ThingUID thingUID = null;
ShellyDeviceProfile profile;
ShellySettingsDevice devInfo;
ShellyApiInterface api = null;
boolean auth = false;
String mac = "";
String model = "";
String mode = "";
String name = hostname;
String deviceName = "";
String thingType = "";
Map<String, Object> properties = new TreeMap<>();

try {
ShellyThingConfiguration config = fillConfig(bindingConfig, ipAddress);
api = gen2 ? new Shelly2ApiRpc(name, config, httpClient) : new Shelly1HttpApi(name, config, httpClient);
api.initialize();
devInfo = api.getDeviceInfo();
mac = getString(devInfo.mac);
model = devInfo.type;
auth = getBool(devInfo.auth);
if (name.isEmpty() || name.startsWith("shellyplusrange")) {
name = devInfo.hostname;
}
if (devInfo.name != null) {
deviceName = devInfo.name;
}

thingType = substringBeforeLast(name, "-");
profile = api.getDeviceProfile(thingType, devInfo);
api.close();
deviceName = profile.name;
mode = devInfo.mode;
properties = ShellyBaseHandler.fillDeviceProperties(profile);

// get thing type from device name
thingUID = ShellyThingCreator.getThingUID(name, model, mode, false);
} catch (ShellyApiException e) {
ShellyApiResult result = e.getApiResult();
if (result.isHttpAccessUnauthorized()) {
// create shellyunknown thing - will be changed during thing initialization with valid credentials
thingUID = ShellyThingCreator.getThingUID(name, model, mode, true);
}
} catch (IllegalArgumentException | IOException e) { // maybe some format description was buggy
logger.debug("Discovery: Unable to discover thing", e);
} finally {
if (api != null) {
api.close();
}
}

if (thingUID != null) {
addProperty(properties, PROPERTY_MAC_ADDRESS, mac);
addProperty(properties, CONFIG_DEVICEIP, ipAddress);
addProperty(properties, PROPERTY_MODEL_ID, model);
addProperty(properties, PROPERTY_SERVICE_NAME, name);
addProperty(properties, PROPERTY_DEV_NAME, deviceName);
addProperty(properties, PROPERTY_DEV_TYPE, thingType);
addProperty(properties, PROPERTY_DEV_GEN, gen2 ? "2" : "1");
addProperty(properties, PROPERTY_DEV_MODE, mode);
addProperty(properties, PROPERTY_DEV_AUTH, auth ? "yes" : "no");

String thingLabel = deviceName.isEmpty() ? name + " - " + ipAddress
: deviceName + " (" + name + "@" + ipAddress + ")";
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(thingLabel)
.withRepresentationProperty(PROPERTY_SERVICE_NAME).build();
}

return null;
}

public static ShellyThingConfiguration fillConfig(ShellyBindingConfiguration bindingConfig, String address)
throws IOException {
ShellyThingConfiguration config = new ShellyThingConfiguration();
config.deviceIp = address;
config.userId = bindingConfig.defaultUserId;
config.password = bindingConfig.defaultPassword;
return config;
}

private static void addProperty(Map<String, Object> properties, String key, @Nullable String value) {
properties.put(key, value != null ? value : "");
}
}