From afc6d949e8bea4b888303c6f1b299d94983b32df Mon Sep 17 00:00:00 2001 From: Markus Michels Date: Sun, 31 Mar 2024 10:27:47 +0200 Subject: [PATCH] =?UTF-8?q?[shelly]=C2=A0Add=20support=20for=20Range=20Ext?= =?UTF-8?q?ender=20feature=20(#16419)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- bundles/org.openhab.binding.shelly/README.md | 21 ++ .../internal/api1/Shelly1ApiJsonDTO.java | 3 + .../internal/api2/Shelly2ApiJsonDTO.java | 16 ++ .../shelly/internal/api2/Shelly2ApiRpc.java | 21 +- .../config/ShellyThingConfiguration.java | 10 + .../ShellyBasicDiscoveryService.java | 197 ++++++++++++++++++ .../discovery/ShellyBluDiscoveryService.java | 92 -------- .../discovery/ShellyDiscoveryParticipant.java | 85 +------- .../discovery/ShellyThingCreator.java | 3 + .../internal/handler/ShellyBaseHandler.java | 56 +++-- .../internal/handler/ShellyThingTable.java | 27 ++- .../main/resources/OH-INF/config/config2.xml | 6 +- .../resources/OH-INF/i18n/shelly.properties | 2 + 13 files changed, 337 insertions(+), 202 deletions(-) create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBasicDiscoveryService.java delete mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBluDiscoveryService.java diff --git a/bundles/org.openhab.binding.shelly/README.md b/bundles/org.openhab.binding.shelly/README.md index 67143fea8e1..e282e73eb1d 100644 --- a/bundles/org.openhab.binding.shelly/README.md +++ b/bundles/org.openhab.binding.shelly/README.md @@ -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. diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java index ffb2e356677..e0e967fe991 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java @@ -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; diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java index 074b69edd50..4fa2b34dc0d 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java @@ -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 apClients; + } + public static class Shelly2DeviceStatus { public class Shelly2InputCounts { public Integer total; diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiRpc.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiRpc.java index 5b654e685c4..3e9d5a223cf 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiRpc.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiRpc.java @@ -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; } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/config/ShellyThingConfiguration.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/config/ShellyThingConfiguration.java index 50ec09b285d..13026629914 100755 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/config/ShellyThingConfiguration.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/config/ShellyThingConfiguration.java @@ -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() ? "" : "***") + ", 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; + } } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBasicDiscoveryService.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBasicDiscoveryService.java new file mode 100644 index 00000000000..27b234cc080 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBasicDiscoveryService.java @@ -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 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 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 properties, String key, @Nullable String value) { + properties.put(key, value != null ? value : ""); + } +} diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBluDiscoveryService.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBluDiscoveryService.java deleted file mode 100644 index 7d2e7794a9b..00000000000 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBluDiscoveryService.java +++ /dev/null @@ -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 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(); - } -} diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyDiscoveryParticipant.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyDiscoveryParticipant.java index bb3436d1377..2bbfc16282e 100755 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyDiscoveryParticipant.java @@ -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 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() ? "" : 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 properties, String key, @Nullable String value) { - properties.put(key, value != null ? value : ""); - } - @Nullable @Override public ThingUID getThingUID(@Nullable ServiceInfo service) throws IllegalArgumentException { diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyThingCreator.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyThingCreator.java index 31728122091..3945c4472a2 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyThingCreator.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyThingCreator.java @@ -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); diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java index e300f5d95cf..7090ba8ccad 100755 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java @@ -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() ? "" : config.userId, - config.password.isEmpty() ? "" : "***", 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 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); } } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyThingTable.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyThingTable.java index 88fabcd1b1c..1be5407c200 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyThingTable.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyThingTable.java @@ -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 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 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); } } diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/config/config2.xml b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/config/config2.xml index ceccff2a0ac..24fbbef7511 100644 --- a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/config/config2.xml +++ b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/config/config2.xml @@ -8,7 +8,6 @@ @text/thing-type.config.shelly.deviceIp.description - network-address @@ -27,6 +26,11 @@ @text/thing-type.config.shelly.enableBluGateway.description false + + + @text/thing-type.config.shelly.enableRangeExtender.description + true + diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties index d87907eede7..ead3eea6be4 100644 --- a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties +++ b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties @@ -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