[shelly] Add support for Range Extender feature (#16419)

* Add support for Shelly Range Extender mode (Plus/Pro series only)
* Check for secondary devices also when manual scan is triggered

Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
Markus Michels 2024-03-31 10:27:47 +02:00 committed by GitHub
parent d2050f43a7
commit afc6d949e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 337 additions and 202 deletions

View File

@ -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 &lt;ub shelly ip&gt;:&lt;special port&gt;.
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.

View File

@ -17,6 +17,7 @@ import java.util.List;
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;
@ -594,6 +595,7 @@ public class Shelly1ApiJsonDTO {
public Boolean wifiRecoveryReboot; // FW 1.10+
@SerializedName("ap_roaming")
public ShellyApRoaming apRoaming; // FW 1.10+
public Boolean rangeExtender; // Gen2: Range extender
public ShellySettingsMqtt mqtt = new ShellySettingsMqtt();
public ShellySettingsSntp sntp = new ShellySettingsSntp();
@ -742,6 +744,7 @@ public class Shelly1ApiJsonDTO {
// /settings/sta for details
public ShellyStatusCloud cloud = new ShellyStatusCloud();
public ShellyStatusMqtt mqtt = new ShellyStatusMqtt();
public Shelly2APClientList rangeExtender;
public String time;
public Integer serial = -1;

View File

@ -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";
@ -520,6 +521,21 @@ public class Shelly2ApiJsonDTO {
public Shelly2GetConfigResult result;
}
public static class Shelly2APClientList {
public static class Shelly2APClient {
public String mac;
public String ip;
@SerializedName("ip_static")
public Boolean staticIP;
public Integer mport;
public Long since;
}
public Long ts;
@SerializedName("ap_clients")
public ArrayList<Shelly2APClient> apClients;
}
public static class Shelly2DeviceStatus {
public class Shelly2InputCounts {
public Integer total;

View File

@ -55,6 +55,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortSta
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;
@ -148,7 +149,9 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
@Override
public void startScan() {
try {
installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway);
if (getProfile().isBlu) {
installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway);
}
} catch (ShellyApiException e) {
}
}
@ -222,6 +225,9 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
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) {
@ -797,6 +803,19 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
}
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;
}

View File

@ -45,4 +45,14 @@ public class ShellyThingConfiguration {
public String serviceName = "";
public Boolean enableBluGateway = false;
public Boolean enableRangeExtender = true;
@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;
}
}

View File

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

View File

@ -1,92 +0,0 @@
/**
* 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.core.thing.Thing.PROPERTY_MAC_ADDRESS;
import java.util.Hashtable;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
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 ShellyBluDiscoveryService}.
*
* @author Markus Michels - Initial Contribution
*
*/
@NonNullByDefault
public class ShellyBluDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(ShellyBluDiscoveryService.class);
private final BundleContext bundleContext;
private final ShellyThingTable thingTable;
private static final int TIMEOUT = 10;
private @Nullable ServiceRegistration<?> discoveryService;
public ShellyBluDiscoveryService(BundleContext bundleContext, ShellyThingTable thingTable) {
super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT);
this.bundleContext = bundleContext;
this.thingTable = thingTable;
}
@SuppressWarnings("null")
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 unregisterDeviceDiscoveryService() {
if (discoveryService != null) {
discoveryService.unregister();
}
}
@Override
public void deactivate() {
super.deactivate();
unregisterDeviceDiscoveryService();
}
}

View File

@ -14,32 +14,20 @@ 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.PROPERTY_MODEL_ID;
import java.io.IOException;
import java.net.Inet4Address;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.jmdns.ServiceInfo;
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.provider.ShellyTranslationProvider;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
@ -109,15 +97,8 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
return null;
}
String address = "";
try {
String mode = "";
String model = "unknown";
String deviceName = "";
ThingUID thingUID = null;
ShellyDeviceProfile profile;
Map<String, Object> properties = new TreeMap<>();
String address = "";
name = service.getName().toLowerCase();
Inet4Address[] hostAddresses = service.getInet4Addresses();
if ((hostAddresses != null) && (hostAddresses.length > 0)) {
@ -145,65 +126,7 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
String gen = getString(service.getPropertyString("gen"));
boolean gen2 = "2".equals(gen) || "3".equals(gen);
ShellyApiInterface api = null;
boolean auth = false;
ShellySettingsDevice devInfo;
try {
api = gen2 ? new Shelly2ApiRpc(name, config, httpClient) : new Shelly1HttpApi(name, config, httpClient);
api.initialize();
devInfo = api.getDeviceInfo();
model = devInfo.type;
gen2 = !(devInfo.gen == 1); // gen 2+3
auth = getBool(devInfo.auth);
if (devInfo.name != null) {
deviceName = devInfo.name;
}
profile = api.getDeviceProfile(thingType, devInfo);
api.close();
logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
deviceName = profile.name;
mode = devInfo.mode;
properties = ShellyBaseHandler.fillDeviceProperties(profile);
logger.trace("{}: thingType={}, deviceType={}, mode={}, symbolic name={}", name, thingType,
devInfo.type, mode.isEmpty() ? "<standard>" : mode, deviceName);
// get thing type from device name
thingUID = ShellyThingCreator.getThingUID(name, model, mode, false);
} catch (ShellyApiException e) {
ShellyApiResult result = e.getApiResult();
if (result.isHttpAccessUnauthorized()) {
logger.info("{}: {}", name, messages.get("discovery.protected", address));
// create shellyunknown thing - will be changed during thing initialization with valid credentials
thingUID = ShellyThingCreator.getThingUID(name, model, mode, true);
} else {
logger.debug("{}: {}", name, messages.get("discovery.failed", address, e.toString()));
}
} catch (IllegalArgumentException e) { // maybe some format description was buggy
logger.debug("{}: Discovery failed!", name, e);
} finally {
if (api != null) {
api.close();
}
}
if (thingUID != null) {
addProperty(properties, CONFIG_DEVICEIP, address);
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");
logger.debug("{}: Adding Shelly {}, UID={}", name, deviceName, thingUID.getAsString());
String thingLabel = deviceName.isEmpty() ? name + " - " + address
: deviceName + " (" + name + "@" + address + ")";
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(thingLabel)
.withRepresentationProperty(PROPERTY_SERVICE_NAME).build();
}
return ShellyBasicDiscoveryService.createResult(gen2, name, address, bindingConfig, httpClient, messages);
} catch (IOException | NullPointerException e) {
// maybe some format description was buggy
logger.debug("{}: Exception on processing serviceInfo '{}'", name, service.getNiceTextString(), e);
@ -211,10 +134,6 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
return null;
}
private void addProperty(Map<String, Object> properties, String key, @Nullable String value) {
properties.put(key, value != null ? value : "");
}
@Nullable
@Override
public ThingUID getThingUID(@Nullable ServiceInfo service) throws IllegalArgumentException {

View File

@ -174,6 +174,7 @@ public class ShellyThingCreator {
public static final String THING_TYPE_SHELLYPLUSI4DC_STR = "shellyplusi4dc";
public static final String THING_TYPE_SHELLYPLUSHT_STR = "shellyplusht";
public static final String THING_TYPE_SHELLYPLUSSMOKE_STR = "shellyplussmoke";
public static final String THING_TYPE_SHELLYPLUSUNI_STR = "shellyplusuni";
public static final String THING_TYPE_SHELLYPLUSPLUGS_STR = "shellyplusplug";
public static final String THING_TYPE_SHELLYPLUSPLUGUS_STR = "shellyplusplugus";
public static final String THING_TYPE_SHELLYPLUSDIMMERUS_STR = "shellypluswdus";
@ -375,6 +376,7 @@ public class ShellyThingCreator {
THING_TYPE_MAPPING.put(SHELLYDT_PLUSI4, THING_TYPE_SHELLYPLUSI4_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSHT, THING_TYPE_SHELLYPLUSHT_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSSMOKE, THING_TYPE_SHELLYPLUSSMOKE_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSUNI, THING_TYPE_SHELLYUNI_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSDIMMERUS, THING_TYPE_SHELLYPLUSDIMMERUS_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSDIMMER10V, THING_TYPE_SHELLYPLUSDIMMER10V_STR);
@ -460,6 +462,7 @@ public class ShellyThingCreator {
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSI4_STR, THING_TYPE_SHELLYPLUSI4_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSHT_STR, THING_TYPE_SHELLYPLUSHT_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSSMOKE_STR, THING_TYPE_SHELLYPLUSSMOKE_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSUNI_STR, THING_TYPE_SHELLYUNI_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSDIMMERUS_STR, THING_TYPE_SHELLYPLUSDIMMERUS_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPLUSDIMMER10V_STR, THING_TYPE_SHELLYPLUSDIMMER10V_STR);

View File

@ -45,15 +45,18 @@ import org.openhab.binding.shelly.internal.api1.Shelly1CoapHandler;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2APClientList.Shelly2APClient;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
import org.openhab.binding.shelly.internal.api2.ShellyBluApi;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.discovery.ShellyBasicDiscoveryService;
import org.openhab.binding.shelly.internal.discovery.ShellyThingCreator;
import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.binding.shelly.internal.util.ShellyChannelCache;
import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
@ -93,6 +96,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
protected final ShellyApiInterface api;
private final HttpClient httpClient;
private final ShellyThingTable thingTable;
private ShellyBindingConfiguration bindingConfig;
protected ShellyThingConfiguration config = new ShellyThingConfiguration();
@ -139,6 +143,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
final Shelly1CoapServer coapServer, final HttpClient httpClient) {
super(thing);
this.thingTable = thingTable;
this.thingName = getString(thing.getLabel());
this.messages = translationProvider;
this.cache = new ShellyChannelCache(this);
@ -177,15 +182,10 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
initJob = scheduler.schedule(() -> {
boolean start = true;
try {
initializeThingConfig();
logger.debug("{}: Device config: Device address={}, HTTP user/password={}/{}, update interval={}",
thingName, config.deviceAddress, config.userId.isEmpty() ? "<non>" : config.userId,
config.password.isEmpty() ? "<none>" : "***", config.updateInterval);
logger.debug(
"{}: Configured Events: Button: {}, Switch (on/off): {}, Push: {}, Roller: {}, Sensor: {}, CoIoT: {}, Enable AutoCoIoT: {}",
thingName, config.eventsButton, config.eventsSwitch, config.eventsPush, config.eventsRoller,
config.eventsSensorReport, config.eventsCoIoT, bindingConfig.autoCoIoT);
start = initializeThing();
if (initializeThingConfig()) {
logger.debug("{}: Config: {}", thingName, config);
start = initializeThing();
}
} catch (ShellyApiException e) {
start = handleApiException(e);
} catch (IllegalArgumentException e) {
@ -253,6 +253,8 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
if (api.isInitialized()) {
api.startScan();
}
checkRangeExtender(profile);
}
/**
@ -356,6 +358,9 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
updateProperties(tmpPrf, tmpPrf.status);
checkVersion(tmpPrf, tmpPrf.status);
// Check for Range Extender mode, add secondary device to Inbox
checkRangeExtender(tmpPrf);
startCoap(config, tmpPrf);
if (!gen2 && !blu) {
api.setActionURLs(); // register event urls
@ -582,6 +587,21 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
}
}
private void checkRangeExtender(ShellyDeviceProfile prf) {
if (getBool(prf.settings.rangeExtender) && config.enableRangeExtender && prf.status.rangeExtender != null
&& prf.status.rangeExtender.apClients != null) {
for (Shelly2APClient client : profile.status.rangeExtender.apClients) {
String secondaryIp = config.deviceIp + ":" + client.mport.toString();
String name = "shellyplusrange-" + client.mac.replaceAll(":", "");
DiscoveryResult result = ShellyBasicDiscoveryService.createResult(true, name, secondaryIp,
bindingConfig, httpClient, messages);
if (result != null) {
thingTable.discoveredResult(result);
}
}
}
}
private void showThingConfig(ShellyDeviceProfile profile) {
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName,
profile.device.hostname, profile.device.type, profile.hwRev, profile.hwBatchId, profile.fwVersion,
@ -955,7 +975,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
/**
* Initialize the binding's thing configuration, calc update counts
*/
protected void initializeThingConfig() {
protected boolean initializeThingConfig() {
thingType = getThing().getThingTypeUID().getId();
final Map<String, String> properties = getThing().getProperties();
thingName = getString(properties.get(PROPERTY_SERVICE_NAME));
@ -970,18 +990,20 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
if (config.deviceAddress.isEmpty()) {
logger.debug("{}: IP/MAC address for the device must not be empty", thingName); // may not set in .things
// file
return;
return false;
}
config.deviceAddress = config.deviceAddress.toLowerCase().replace(":", ""); // remove : from MAC address and
// convert to lower case
if (!config.deviceIp.isEmpty()) {
try {
InetAddress addr = InetAddress.getByName(config.deviceIp);
String ip = config.deviceIp.contains(":") ? substringBefore(config.deviceIp, ":") : config.deviceIp;
String port = config.deviceIp.contains(":") ? substringAfter(config.deviceIp, ":") : "";
InetAddress addr = InetAddress.getByName(ip);
String saddr = addr.getHostAddress();
if (!config.deviceIp.equals(saddr)) {
if (!ip.equals(saddr)) {
logger.debug("{}: hostname {} resolved to IP address {}", thingName, config.deviceIp, saddr);
config.deviceIp = saddr;
config.deviceIp = saddr + (port.isEmpty() ? ip : ip + ":" + port);
}
} catch (UnknownHostException e) {
logger.debug("{}: Unable to resolve hostname {}", thingName, config.deviceIp);
@ -994,7 +1016,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
if (config.localIp.startsWith("169.254")) {
setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "config-status.error.network-config",
config.localIp);
return;
return false;
}
if (!profile.isGen2 && config.userId.isEmpty() && !bindingConfig.defaultUserId.isEmpty()) {
@ -1028,6 +1050,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
skipCount = config.updateInterval / UPDATE_STATUS_INTERVAL_SECONDS;
logger.trace("{}: updateInterval = {}s -> skipCount = {}", thingName, config.updateInterval, skipCount);
return true;
}
private void checkVersion(ShellyDeviceProfile prf, ShellySettingsStatus status) {
@ -1123,6 +1146,9 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
properties.replace(PROPERTY_DEV_MODE, mode);
updateProperties(properties);
changeThingType(thingTypeUID, getConfig());
} else {
logger.debug("{}: to {}", thingName, thingType);
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Unable to change thing type to " + thingType);
}
}

View File

@ -17,7 +17,8 @@ import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.discovery.ShellyBluDiscoveryService;
import org.openhab.binding.shelly.internal.discovery.ShellyBasicDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.thing.ThingTypeUID;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Component;
@ -34,7 +35,7 @@ import org.osgi.service.component.annotations.Deactivate;
@Component(service = ShellyThingTable.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
public class ShellyThingTable {
private Map<String, ShellyThingInterface> thingTable = new ConcurrentHashMap<>();
private @Nullable ShellyBluDiscoveryService bluDiscoveryService;
private @Nullable ShellyBasicDiscoveryService discoveryService;
public void addThing(String key, ShellyThingInterface thing) {
if (thingTable.containsKey(key)) {
@ -80,9 +81,9 @@ public class ShellyThingTable {
}
public void startDiscoveryService(BundleContext bundleContext) {
if (bluDiscoveryService == null) {
bluDiscoveryService = new ShellyBluDiscoveryService(bundleContext, this);
bluDiscoveryService.registerDeviceDiscoveryService();
if (discoveryService == null) {
discoveryService = new ShellyBasicDiscoveryService(bundleContext, this);
discoveryService.registerDeviceDiscoveryService();
}
}
@ -93,16 +94,22 @@ public class ShellyThingTable {
}
public void stopDiscoveryService() {
if (bluDiscoveryService != null) {
bluDiscoveryService.unregisterDeviceDiscoveryService();
bluDiscoveryService = null;
if (discoveryService != null) {
discoveryService.unregisterDeviceDiscoveryService();
discoveryService = null;
}
}
public void discoveredResult(ThingTypeUID uid, String model, String serviceName, String address,
Map<String, Object> properties) {
if (bluDiscoveryService != null) {
bluDiscoveryService.discoveredResult(uid, model, serviceName, address, properties);
if (discoveryService != null) {
discoveryService.discoveredResult(uid, model, serviceName, address, properties);
}
}
public void discoveredResult(DiscoveryResult result) {
if (discoveryService != null) {
discoveryService.discoveredResult(result);
}
}

View File

@ -8,7 +8,6 @@
<parameter name="deviceIp" type="text" required="true">
<label>@text/thing-type.config.shelly.deviceIp.label</label>
<description>@text/thing-type.config.shelly.deviceIp.description</description>
<context>network-address</context>
</parameter>
<parameter name="password" type="text" required="false">
<label>@text/thing-type.config.shelly.password.label</label>
@ -27,6 +26,11 @@
<description>@text/thing-type.config.shelly.enableBluGateway.description</description>
<default>false</default>
</parameter>
<parameter name="enableRangeExtender" type="boolean" required="false">
<label>@text/thing-type.config.shelly.enableRangeExtender.label</label>
<description>@text/thing-type.config.shelly.enableRangeExtender.description</description>
<default>true</default>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:roller-gen2">

View File

@ -141,6 +141,8 @@ thing-type.config.shelly.updateInterval.label = Status Interval
thing-type.config.shelly.updateInterval.description = Interval for the device status update
thing-type.config.shelly.enableBluGateway.label = Enable BLU Gateway Support
thing-type.config.shelly.enableBluGateway.description = Enables BLU Gateway support including auto-upload of the required script
thing-type.config.shelly.enableRangeExtender.label = Enable Range Extender Support
thing-type.config.shelly.enableRangeExtender.description = Auto discovers devices, which are connected using the Shelly Range Extender support
thing-type.config.shelly.eventsButton.label = Button Events
thing-type.config.shelly.eventsButton.description = Activates the Button Action URLS
thing-type.config.shelly.eventsPush.label = Push Events