]> git.basschouten.com Git - openhab-addons.git/commitdiff
[shelly] Add support for Range Extender feature (#16419)
authorMarkus Michels <markus7017@gmail.com>
Sun, 31 Mar 2024 08:27:47 +0000 (10:27 +0200)
committerGitHub <noreply@github.com>
Sun, 31 Mar 2024 08:27:47 +0000 (10:27 +0200)
* 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>
13 files changed:
bundles/org.openhab.binding.shelly/README.md
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiRpc.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/config/ShellyThingConfiguration.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBasicDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBluDiscoveryService.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyDiscoveryParticipant.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyThingCreator.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyThingTable.java
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/config/config2.xml
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties

index 67143fea8e1c476eacc52c985b627a6a7ca41564..e282e73eb1d13c5d4ec6c18ccf9a062394d23150 100644 (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.
index ffb2e3566772c191644268ba8944a48d5c9c277a..e0e967fe991f49fe202ac523802f4844c32c9c9f 100644 (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;
index 074b69edd50823ca7f3484eaf7820142d2706fa2..4fa2b34dc0dcba65bcd0b30057055441ea4de4ff 100644 (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;
index 5b654e685c4197e7aed85917073b900760229f90..3e9d5a223cf03a950d2aa489a6ddf52d0458a0ae 100644 (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;
     }
 
index 50ec09b285d84104d3faa499dbb200081fe22fdc..130266299142da442297bb7663ff482fc73e589f 100755 (executable)
@@ -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;
+    }
 }
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 (file)
index 0000000..27b234c
--- /dev/null
@@ -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 : "");
+    }
+}
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 (file)
index 7d2e779..0000000
+++ /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<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();
-    }
-}
index bb3436d1377340e72fa3efc8574aac663994c640..2bbfc16282e6d3c634d8eb34940d75b0db1d6648 100755 (executable)
@@ -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 {
index 31728122091f630b0d60fa4022f26a8d24219abc..3945c4472a288e9aa2c30a20d3f05c540e33cde2 100644 (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);
 
index e300f5d95cf622d8a608722c10a5d6618684102c..7090ba8ccadbff54f6fcb3101d7c7fdac5898c4f 100755 (executable)
@@ -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);
         }
     }
 
index 88fabcd1b1cec053f0d3d86c8af3c1318a03bc22..1be5407c200f0c3dab3298efbd7ced9fd7a2e739 100644 (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);
         }
     }
 
index ceccff2a0acaecbac974e268ec8e1ce51f640cad..24fbbef7511fcc63ede948cebaca6c4b14371f7e 100644 (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>
                        <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">
index d87907eede7b473a324a1b52c9568e37909f9422..ead3eea6be43365c89ffe7e7d1f4bfa36a331809 100644 (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