]> git.basschouten.com Git - openhab-addons.git/commitdiff
[shelly] Fix Gen2 auth, improved security for Gen1 auth, improved discovery (#15898)
authorMarkus Michels <markus7017@gmail.com>
Sat, 18 Nov 2023 15:28:44 +0000 (16:28 +0100)
committerGitHub <noreply@github.com>
Sat, 18 Nov 2023 15:28:44 +0000 (16:28 +0100)
Signed-off-by: Markus Michels <markus7017@gmail.com>
18 files changed:
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiException.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiInterface.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpClient.java
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/api1/Shelly1CoapHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1HttpApi.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.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/api2/ShellyBluApi.java
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/handler/ShellyBaseHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyLightHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerActionPage.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOtaPage.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOverviewPage.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerPage.java

index 845219bf92c8bf2478b1f2cbf70941b589fe6a8d..6c6dc2a9588de7ad9dd9f602645e0c1895424803 100644 (file)
@@ -118,6 +118,10 @@ public class ShellyApiException extends Exception {
                 || exType == NoRouteToHostException.class;
     }
 
+    public boolean isNoRouteToHost() {
+        return getCauseClass() == NoRouteToHostException.class;
+    }
+
     public boolean isUnknownHost() {
         return getCauseClass() == UnknownHostException.class;
     }
index 8b7716007d83ae37f713da7727f6160da5bc35a2..1f9443824e41a008b4fb0f637c093890c7c5e7dd 100644 (file)
@@ -15,6 +15,7 @@ package org.openhab.binding.shelly.internal.api;
 import java.util.Map;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
@@ -42,7 +43,8 @@ public interface ShellyApiInterface {
 
     ShellySettingsDevice getDeviceInfo() throws ShellyApiException;
 
-    ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException;
+    ShellyDeviceProfile getDeviceProfile(String thingType, @Nullable ShellySettingsDevice device)
+            throws ShellyApiException;
 
     ShellySettingsStatus getStatus() throws ShellyApiException;
 
index 814917e26bad76b1504f8368d4e2e2867e481fd2..6f96910b560591dacd3c535ef041f76b15e2fa0a 100644 (file)
@@ -24,6 +24,7 @@ import java.util.regex.Pattern;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsGlobal;
 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
@@ -47,24 +48,21 @@ import com.google.gson.Gson;
 @NonNullByDefault
 public class ShellyDeviceProfile {
     private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class);
-    private static final Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+(-[a-z0-9]*)?");
+    private static final Pattern GEN1_VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+(-[a-z0-9]*)?");
+    private static final Pattern GEN2_VERSION_PATTERN = Pattern.compile("\\d+\\.\\d+\\.\\d+(-[a-fh-z0-9]*)?");
 
     public boolean initialized = false; // true when initialized
 
     public String thingName = "";
-    public String deviceType = "";
     public boolean extFeatures = false;
 
     public String settingsJson = "";
+    public ShellySettingsDevice device = new ShellySettingsDevice();
     public ShellySettingsGlobal settings = new ShellySettingsGlobal();
     public ShellySettingsStatus status = new ShellySettingsStatus();
 
-    public String hostname = "";
     public String name = "";
-    public String model = "";
-    public String mode = "";
     public boolean discoverable = true;
-    public boolean auth = false;
     public boolean alwaysOn = true;
     public boolean isGen2 = false;
     public boolean isBlu = false;
@@ -72,7 +70,6 @@ public class ShellyDeviceProfile {
 
     public String hwRev = "";
     public String hwBatchId = "";
-    public String mac = "";
     public String fwVersion = "";
     public String fwDate = "";
 
@@ -118,10 +115,13 @@ public class ShellyDeviceProfile {
     public ShellyDeviceProfile() {
     }
 
-    public ShellyDeviceProfile initialize(String thingType, String jsonIn) throws ShellyApiException {
+    public ShellyDeviceProfile initialize(String thingType, String jsonIn, @Nullable ShellySettingsDevice device)
+            throws ShellyApiException {
         Gson gson = new Gson();
-
         initialized = false;
+        if (device != null) {
+            this.device = device;
+        }
 
         initFromThingType(thingType);
 
@@ -141,36 +141,36 @@ public class ShellyDeviceProfile {
         settings = fromJson(gson, json, ShellySettingsGlobal.class);
 
         // General settings
+        if (getString(device.hostname).isEmpty() && !getString(device.mac).isEmpty()) {
+            device.hostname = device.mac.length() >= 12 ? "shelly-" + device.mac.toUpperCase().substring(6, 11)
+                    : "unknown";
+        }
         name = getString(settings.name);
-        deviceType = getString(settings.device.type);
-        mac = getString(settings.device.mac);
-        hostname = !getString(settings.device.hostname).isEmpty() ? settings.device.hostname.toLowerCase()
-                : mac.length() >= 12 ? "shelly-" + mac.toUpperCase().substring(6, 11) : "unknown";
-        mode = getString(settings.mode).toLowerCase();
         hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : "";
         hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
-        fwDate = substringBefore(settings.fw, "/");
-        fwVersion = extractFwVersion(settings.fw);
+        fwDate = substringBefore(device.fw, "-");
+        fwVersion = extractFwVersion(device.fw);
         ShellyVersionDTO version = new ShellyVersionDTO();
         extFeatures = version.compare(fwVersion, SHELLY_API_FW_110) >= 0;
         discoverable = (settings.discoverable == null) || settings.discoverable;
 
+        String mode = getString(device.mode);
         isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER);
         inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
 
-        numRelays = !isLight ? getInteger(settings.device.numOutputs) : 0;
+        numRelays = !isLight ? getInteger(device.numOutputs) : 0;
         if ((numRelays > 0) && (settings.relays == null)) {
             numRelays = 0;
         }
         hasRelays = (numRelays > 0) || isDimmer;
-        numRollers = getInteger(settings.device.numRollers);
+        numRollers = getInteger(device.numRollers);
         numInputs = settings.inputs != null ? settings.inputs.size() : hasRelays ? isRoller ? 2 : 1 : 0;
 
         isEMeter = settings.emeters != null;
-        numMeters = !isEMeter ? getInteger(settings.device.numMeters) : getInteger(settings.device.numEMeters);
+        numMeters = !isEMeter ? getInteger(device.numMeters) : getInteger(device.numEMeters);
         if ((numMeters == 0) && isLight) {
             // RGBW2 doesn't report, but has one
-            numMeters = inColor ? 1 : getInteger(settings.device.numOutputs);
+            numMeters = inColor ? 1 : getInteger(device.numOutputs);
         }
 
         initialized = true;
@@ -199,8 +199,9 @@ public class ShellyDeviceProfile {
         isGen2 = isGeneration2(thingType);
         isBlu = isBluSeries(thingType); // e.g. SBBT for BLU Button
 
-        isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2)
-                || deviceType.equalsIgnoreCase(SHELLYDT_PLUSDIMMERUS)
+        String type = getString(device.type);
+        isDimmer = type.equalsIgnoreCase(SHELLYDT_DIMMER) || type.equalsIgnoreCase(SHELLYDT_DIMMER2)
+                || type.equalsIgnoreCase(SHELLYDT_PLUSDIMMERUS)
                 || thingType.equalsIgnoreCase(THING_TYPE_SHELLYPLUSDIMMERUS_STR);
         isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
         isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR)
@@ -390,7 +391,8 @@ public class ShellyDeviceProfile {
                     .replace("/v1.12-", "/v1.12.0");
 
             // Extract version from string, e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
-            Matcher matcher = VERSION_PATTERN.matcher(vers);
+            Matcher matcher = version.startsWith("v") ? GEN1_VERSION_PATTERN.matcher(vers)
+                    : GEN2_VERSION_PATTERN.matcher(vers);
             if (matcher.find()) {
                 return matcher.group(0);
             }
index 5b04bc58a604b36e8aa3ee0013731f5900b17fc9..7ed4363450bc8b1778e92004bf82f2ae541d2fee 100644 (file)
@@ -69,6 +69,7 @@ public class ShellyHttpClient {
     protected int timeoutErrors = 0;
     protected int timeoutsRecovered = 0;
     private ShellyDeviceProfile profile;
+    protected boolean basicAuth = false;
 
     public ShellyHttpClient(String thingName, ShellyThingInterface thing) {
         this(thingName, thing.getThingConfig(), thing.getHttpClient());
@@ -83,9 +84,6 @@ public class ShellyHttpClient {
         this.httpClient.setConnectTimeout(SHELLY_API_TIMEOUT_MS);
     }
 
-    public void initialize() throws ShellyApiException {
-    }
-
     public void setConfig(String thingName, ShellyThingConfiguration config) {
         this.thingName = thingName;
         this.config = config;
@@ -167,7 +165,7 @@ public class ShellyHttpClient {
                     authHeader = formatAuthResponse(uri,
                             buildAuthResponse(uri, auth, SHELLY2_AUTHDEF_USER, config.password));
                 } else {
-                    if (!uri.equals(SHELLYRPC_ENDPOINT)) {
+                    if (basicAuth) {
                         String bearer = config.userId + ":" + config.password;
                         authHeader = HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(bearer.getBytes());
                     }
index c5304fe4e1ce102c3d4d84af8a37eefe61351219..d836e728f2c4dae6273ee06748507899940b73bc 100644 (file)
@@ -261,6 +261,10 @@ public class Shelly1ApiJsonDTO {
 
     public static class ShellySettingsDevice {
         public String type;
+        public String mode; // Gen 1
+        public String id; // Gen2: service name
+        public String name; // Gen2: configured device name
+        public String profile; // Gen 2
         public String mac;
         public String hostname;
         public String fw;
@@ -563,7 +567,6 @@ public class Shelly1ApiJsonDTO {
 
     public static class ShellySettingsGlobal {
         // https://shelly-api-docs.shelly.cloud/#shelly1pm-settings
-        public ShellySettingsDevice device = new ShellySettingsDevice();
         @SerializedName("wifi_ap")
         public ShellySettingsWiFiAp wifiAp = new ShellySettingsWiFiAp();
         @SerializedName("wifi_sta")
index 23c9cdede0164b886d2060d610af633f699eb71a..3de692e224034d5dc6a44f1dc14ff62fe6fe1d86 100644 (file)
@@ -182,10 +182,10 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
             for (Option opt : options) {
                 if (opt.getNumber() == COIOT_OPTION_GLOBAL_DEVID) {
                     String devid = opt.getStringValue();
-                    if (devid.contains("#") && profile.mac != null) {
+                    if (devid.contains("#") && profile.device.mac != null) {
                         // Format: <device type>#<mac address>#<coap version>
                         String macid = substringBetween(devid, "#", "#");
-                        if (profile.mac.toUpperCase().contains(macid.toUpperCase())) {
+                        if (getString(profile.device.mac).toUpperCase().contains(macid.toUpperCase())) {
                             match = true;
                             break;
                         }
index 69a4d7bd50110867ca83f571232fa8522249f9de..330b2be360618bce90fe720ef60c8c3cdcfc8c4f 100644 (file)
@@ -22,6 +22,7 @@ import java.util.HashMap;
 import java.util.Map;
 
 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;
@@ -79,10 +80,26 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
         this.profile = new ShellyDeviceProfile();
     }
 
+    @Override
+    public void initialize() throws ShellyApiException {
+        profile.device = getDeviceInfo();
+    }
+
     @Override
     public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
         ShellySettingsDevice info = callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
         info.gen = 1;
+        basicAuth = getBool(info.auth);
+
+        if (getString(info.mode).isEmpty()) { // older Gen1 Firmware
+            if (getInteger(info.numRollers) > 0) {
+                info.mode = SHELLY_CLASS_ROLLER;
+            } else if (getInteger(info.numOutputs) > 0) {
+                info.mode = SHELLY_CLASS_RELAY;
+            } else {
+                info.mode = "";
+            }
+        }
         return info;
     }
 
@@ -104,7 +121,14 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
      * @throws ShellyApiException
      */
     @Override
-    public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
+    public ShellyDeviceProfile getDeviceProfile(String thingType, @Nullable ShellySettingsDevice device)
+            throws ShellyApiException {
+        if (device != null) {
+            profile.device = device;
+        }
+        if (profile.device.type == null) {
+            profile.device = getDeviceInfo();
+        }
         String json = httpRequest(SHELLY_URL_SETTINGS);
         if (json.contains("\"type\":\"SHDM-")) {
             logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName);
@@ -112,10 +136,10 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
         }
 
         // Map settings to device profile for Light and Sense
-        profile.initialize(thingType, json);
+        profile.initialize(thingType, json, profile.device);
 
         // 2nd level initialization
-        profile.thingName = profile.hostname;
+        profile.thingName = profile.device.hostname;
         if (profile.isLight && (profile.numMeters == 0)) {
             logger.debug("{}: Get number of meters from light status", thingName);
             ShellyStatusLight status = getLightStatus();
@@ -396,10 +420,10 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
      */
     @Override
     public void setLightMode(String mode) throws ShellyApiException {
-        if (!mode.isEmpty() && !profile.mode.equals(mode)) {
+        if (!mode.isEmpty() && !profile.device.mode.equals(mode)) {
             setLightSetting(SHELLY_API_MODE, mode);
-            profile.mode = mode;
-            profile.inColor = profile.isLight && profile.mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
+            profile.device.mode = mode;
+            profile.inColor = profile.isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
         }
     }
 
index a8c771fe67ff6e086c255f6086365bb7d6d87991..2805d1310107f4f6969c97c00a5e357eae3e90fc 100644 (file)
@@ -148,6 +148,11 @@ public class Shelly2ApiClient extends ShellyHttpClient {
         MAP_ROLLER_STATE.put(SHELLY2_RSTATE_STOPPED, SHELLY_RSTATE_STOP);
         MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CALIB, SHELLY2_RSTATE_CALIB); // Gen2-only
     }
+    protected static final Map<String, String> MAP_PROFILE = new HashMap<>();
+    static {
+        MAP_PROFILE.put(SHELLY_CLASS_RELAY, SHELLY2_PROFILE_RELAY);
+        MAP_PROFILE.put(SHELLY_CLASS_ROLLER, SHELLY2_PROFILE_COVER);
+    }
 
     protected @Nullable ArrayList<@Nullable ShellySettingsRelay> fillRelaySettings(ShellyDeviceProfile profile,
             Shelly2GetConfigResult dc) {
index 672f9e3b8cc962a52d5957a8f064e935a80d0ee4..ea309929f51b24762f6e7303a7b90aa4f88806f9 100644 (file)
@@ -82,7 +82,7 @@ public class Shelly2ApiJsonDTO {
 
     // Component types
     public static final String SHELLY2_PROFILE_RELAY = "switch";
-    public static final String SHELLY2_PROFILE_ROLLER = "cover";
+    public static final String SHELLY2_PROFILE_COVER = "cover";
 
     // Button types/modes
     public static final String SHELLY2_BTNT_MOMENTARY = "momentary";
@@ -183,13 +183,14 @@ public class Shelly2ApiJsonDTO {
         public String id;
         public String mac;
         public String model;
+        public String profile;
         public Integer gen;
         @SerializedName("fw_id")
-        public String firmware;
+        public String fw;
         public String ver;
         public String app;
         @SerializedName("auth_en")
-        public Boolean authEnable;
+        public Boolean auth;
         @SerializedName("auth_domain")
         public String authDomain;
     }
index d35273bcccccb70d6588d23e11257b349a6f3bd7..17efc1d84ebfab1f3e137c0ed67e2f1035ae168a 100644 (file)
@@ -113,11 +113,6 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
         this.thingName = thingName;
         this.thing = thing;
         this.thingTable = thingTable;
-        try {
-            getProfile().initFromThingType(thing.getThingType());
-        } catch (ShellyApiException e) {
-            logger.info("{}: Shelly2 API initialization failed!", thingName, e);
-        }
     }
 
     /**
@@ -161,9 +156,17 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
 
     @SuppressWarnings("null")
     @Override
-    public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
+    public ShellyDeviceProfile getDeviceProfile(String thingType, @Nullable ShellySettingsDevice devInfo)
+            throws ShellyApiException {
         ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
 
+        if (devInfo != null) {
+            profile.device = devInfo;
+        }
+        if (profile.device.type == null) {
+            profile.device = getDeviceInfo();
+        }
+
         Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class);
         profile.isGen2 = true;
         profile.settingsJson = gson.toJson(dc);
@@ -195,29 +198,18 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
         profile.numRelays = profile.settings.relays != null ? profile.settings.relays.size() : 0;
         profile.numRollers = profile.settings.rollers != null ? profile.settings.rollers.size() : 0;
         profile.hasRelays = profile.numRelays > 0 || profile.numRollers > 0;
-        profile.mode = "";
-        if (profile.hasRelays) {
-            profile.mode = profile.isRoller ? SHELLY_CLASS_ROLLER : SHELLY_CLASS_RELAY;
-        }
-
-        ShellySettingsDevice device = getDeviceInfo();
-        profile.settings.device = device;
-        if (!getString(device.fw).isEmpty()) {
-            profile.fwDate = substringBefore(device.fw, "/");
-            profile.fwVersion = profile.status.update.oldVersion = "v" + substringAfter(device.fw, "/");
+        if (getString(profile.device.mode).isEmpty() && profile.hasRelays) {
+            profile.device.mode = profile.isRoller ? SHELLY_CLASS_ROLLER : SHELLY_CLASS_RELAY;
         }
 
-        profile.hostname = device.hostname;
-        profile.deviceType = device.type;
-        profile.mac = device.mac;
-        profile.auth = device.auth;
+        ShellySettingsDevice device = profile.device;
         profile.isGen2 = device.gen == 2;
         if (config.serviceName.isEmpty()) {
-            config.serviceName = getString(profile.hostname);
+            config.serviceName = getString(profile.device.hostname);
         }
-        profile.fwDate = substringBefore(device.fw, "/");
-        profile.fwVersion = substringBefore(ShellyDeviceProfile.extractFwVersion(device.fw.replace("/", "/v")), "-");
-        profile.status.update.oldVersion = profile.fwVersion;
+        profile.settings.fw = device.fw;
+        profile.fwDate = substringBefore(substringBefore(device.fw, "/"), "-");
+        profile.fwVersion = profile.status.update.oldVersion = ShellyDeviceProfile.extractFwVersion(device.fw);
         profile.status.hasUpdate = profile.status.update.hasUpdate = false;
 
         if (dc.eth != null) {
@@ -741,11 +733,13 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
         Shelly2DeviceSettings device = callApi("/shelly", Shelly2DeviceSettings.class);
         ShellySettingsDevice info = new ShellySettingsDevice();
         info.hostname = getString(device.id);
-        info.fw = getString(device.firmware);
+        info.name = getString(device.name);
+        info.fw = getString(device.fw);
         info.type = getString(device.model);
         info.mac = getString(device.mac);
-        info.auth = getBool(device.authEnable);
+        info.auth = getBool(device.auth);
         info.gen = getInteger(device.gen);
+        info.mode = mapValue(MAP_PROFILE, device.profile);
         return info;
     }
 
@@ -770,16 +764,16 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
             profile.settings.sleepMode.period = ds.sys.wakeupPeriod / 60;
         }
 
-        status.hasUpdate = status.update.hasUpdate = false;
-        status.update.oldVersion = getProfile().fwVersion;
         if (ds.sys.availableUpdates != null) {
             status.update.hasUpdate = ds.sys.availableUpdates.stable != null;
             if (ds.sys.availableUpdates.stable != null) {
-                status.update.newVersion = "v" + getString(ds.sys.availableUpdates.stable.version);
+                status.update.newVersion = ShellyDeviceProfile
+                        .extractFwVersion(getString(ds.sys.availableUpdates.stable.version));
                 status.hasUpdate = new ShellyVersionDTO().compare(profile.fwVersion, status.update.newVersion) < 0;
             }
             if (ds.sys.availableUpdates.beta != null) {
-                status.update.betaVersion = "v" + getString(ds.sys.availableUpdates.beta.version);
+                status.update.betaVersion = ShellyDeviceProfile
+                        .extractFwVersion(getString(ds.sys.availableUpdates.beta.version));
                 status.hasUpdate = new ShellyVersionDTO().compare(profile.fwVersion, status.update.betaVersion) < 0;
             }
         }
index 533d4448e89406868fb01ad4ddc84893d6953575..5eabce2a73d53b5f5e3d6e165362c710ee849eb5 100644 (file)
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
@@ -119,9 +120,13 @@ public class ShellyBluApi extends Shelly2ApiRpc {
     }
 
     @Override
-    public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
+    public ShellyDeviceProfile getDeviceProfile(String thingType, @Nullable ShellySettingsDevice devInfo)
+            throws ShellyApiException {
         ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
 
+        if (devInfo != null) {
+            profile.device = devInfo;
+        }
         profile.isBlu = true;
         profile.settingsJson = "{}";
         profile.thingName = thingName;
@@ -131,13 +136,8 @@ public class ShellyBluApi extends Shelly2ApiRpc {
         }
 
         ShellySettingsDevice device = getDeviceInfo();
-        profile.settings.device = device;
-        profile.hostname = device.hostname;
-        profile.deviceType = device.type;
-        profile.mac = device.mac;
-        profile.auth = device.auth;
         if (config.serviceName.isEmpty()) {
-            config.serviceName = getString(profile.hostname);
+            config.serviceName = getString(profile.device.hostname);
         }
         profile.fwDate = substringBefore(device.fw, "/");
         profile.fwVersion = substringBefore(ShellyDeviceProfile.extractFwVersion(device.fw.replace("/", "/v")), "-");
index 967a6586419498aea32363b26df2aa864bc32175..793b710f7db9e46274832217e2d2fe261ed42716 100755 (executable)
@@ -31,6 +31,7 @@ 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;
@@ -144,17 +145,23 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
 
             boolean gen2 = "2".equals(service.getPropertyString("gen"));
             ShellyApiInterface api = null;
+            ShellySettingsDevice devInfo;
             try {
                 api = gen2 ? new Shelly2ApiRpc(name, config, httpClient) : new Shelly1HttpApi(name, config, httpClient);
                 api.initialize();
-                profile = api.getDeviceProfile(thingType);
+                devInfo = api.getDeviceInfo();
+                model = devInfo.type;
+                if (devInfo.name != null) {
+                    deviceName = devInfo.name;
+                }
+                profile = api.getDeviceProfile(thingType, devInfo);
+                api.close();
                 logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
                 deviceName = profile.name;
-                model = profile.deviceType;
-                mode = profile.mode;
+                mode = devInfo.mode;
                 properties = ShellyBaseHandler.fillDeviceProperties(profile);
                 logger.trace("{}: thingType={}, deviceType={}, mode={}, symbolic name={}", name, thingType,
-                        profile.deviceType, mode.isEmpty() ? "<standard>" : mode, deviceName);
+                        devInfo.type, mode.isEmpty() ? "<standard>" : mode, deviceName);
 
                 // get thing type from device name
                 thingUID = ShellyThingCreator.getThingUID(name, model, mode, false);
index c76a181e45876651431176afeacf150c3075088b..1a35a432b1afc619e2da7b4aae031fceb7e625a9 100755 (executable)
@@ -273,45 +273,41 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
         updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING,
                 messages.get("status.unknown.initializing"));
 
-        profile.initFromThingType(thingType); // do some basic initialization
-
         // Gen 1 only: Setup CoAP listener to we get the CoAP message, which triggers initialization even the thing
         // could not be fully initialized here. In this case the CoAP messages triggers auto-initialization (like the
         // Action URL does when enabled)
+        profile.initFromThingType(thingType);
         if (coap != null && config.eventsCoIoT && !profile.alwaysOn) {
             coap.start(thingName, config);
         }
 
         // Initialize API access, exceptions will be catched by initialize()
         api.initialize();
-        ShellySettingsDevice devInfo = api.getDeviceInfo();
-        if (getBool(devInfo.auth) && config.password.isEmpty()) {
+        ShellySettingsDevice device = profile.device = api.getDeviceInfo();
+        if (getBool(device.auth) && config.password.isEmpty()) {
             setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-no-credentials");
             return false;
         }
         if (config.serviceName.isEmpty()) {
-            config.serviceName = getString(profile.hostname).toLowerCase();
+            config.serviceName = getString(device.hostname).toLowerCase();
         }
 
         api.setConfig(thingName, config);
-        ShellyDeviceProfile tmpPrf = api.getDeviceProfile(thingType);
-        tmpPrf.isGen2 = gen2;
-        tmpPrf.auth = devInfo.auth; // missing in /settings
-
+        ShellyDeviceProfile tmpPrf = api.getDeviceProfile(thingType, profile.device);
+        String mode = getString(tmpPrf.device.mode);
         if (this.getThing().getThingTypeUID().equals(THING_TYPE_SHELLYPROTECTED)) {
-            changeThingType(thingName, tmpPrf.mode);
+            changeThingType(thingName, mode);
             return false; // force re-initialization
         }
         // Validate device mode
         String reqMode = thingType.contains("-") ? substringAfter(thingType, "-") : "";
-        if (!reqMode.isEmpty() && !tmpPrf.mode.equals(reqMode)) {
-            setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-wrong-mode", tmpPrf.mode,
-                    reqMode);
+        if (!reqMode.isEmpty() && !mode.equals(reqMode)) {
+            setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-wrong-mode", mode, reqMode);
             return false;
         }
-        if (!getString(devInfo.coiot).isEmpty()) {
+        if (!getString(tmpPrf.device.coiot).isEmpty()) {
             // New Shelly devices might use a different endpoint for the CoAP listener
-            tmpPrf.coiotEndpoint = devInfo.coiot;
+            tmpPrf.coiotEndpoint = tmpPrf.device.coiot;
         }
         if (tmpPrf.settings.sleepMode != null && !tmpPrf.isTRV) {
             // Sensor, usually 12h, H&T in USB mode 10min
@@ -566,13 +562,10 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
                 status = "offline.conf-error-access-denied";
             } else if (isWatchdogStarted()) {
                 if (!isWatchdogExpired()) {
+                    logger.debug("{}: Ignore API Timeout on {} {}, retry later", thingName, res.method, res.url);
                     if (profile.alwaysOn) { // suppress for battery powered sensors
                         logger.debug("{}: Ignore API Timeout on {} {}, retry later", thingName, res.method, res.url);
                     }
-                } else {
-                    if (isThingOnline()) {
-                        status = "offline.status-error-watchdog";
-                    }
                 }
             } else if (e.isJSONException()) {
                 status = "offline.status-error-unexpected-api-result";
@@ -606,22 +599,18 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
 
     private void showThingConfig(ShellyDeviceProfile profile) {
         logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName,
-                profile.hostname, profile.deviceType, profile.hwRev, profile.hwBatchId, profile.fwVersion,
+                profile.device.hostname, profile.device.type, profile.hwRev, profile.hwBatchId, profile.fwVersion,
                 profile.fwDate);
-        logger.debug("{}: Shelly settings info for {}: {}", thingName, profile.hostname, profile.settingsJson);
-        logger.debug(
-                """
-                        {}: Device \
-                        hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{}), ext. Switch Add-On: {}\
-                        ,isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}, BLU Gateway support: {}\
-                        ,alwaysOn:{}, updatePeriod:{}sec\
-                        """,
-                thingName, profile.hasRelays, profile.numRelays, profile.isRoller, profile.numRollers, profile.isDimmer,
-                profile.numMeters, profile.isEMeter, profile.settings.extSwitch != null ? "installed" : "n/a",
-                profile.isSensor, profile.isDW, profile.hasBattery,
-                profile.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "", profile.isSense,
-                profile.isMotion, profile.isLight, profile.isBulb, profile.isDuo, profile.isRGBW2, profile.inColor,
-                profile.alwaysOn, profile.updatePeriod, config.enableBluGateway);
+        logger.debug("{}: Shelly settings info for {}: {}", thingName, profile.device.hostname, profile.settingsJson);
+        logger.debug("{}: Device "
+                + "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{}), ext. Switch Add-On: {}"
+                + ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}, BLU Gateway support: {}"
+                + ",alwaysOn:{}, updatePeriod:{}sec", thingName, profile.hasRelays, profile.numRelays, profile.isRoller,
+                profile.numRollers, profile.isDimmer, profile.numMeters, profile.isEMeter,
+                profile.settings.extSwitch != null ? "installed" : "n/a", profile.isSensor, profile.isDW,
+                profile.hasBattery, profile.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "",
+                profile.isSense, profile.isMotion, profile.isLight, profile.isBulb, profile.isDuo, profile.isRGBW2,
+                profile.inColor, profile.alwaysOn, profile.updatePeriod, config.enableBluGateway);
         if (profile.status.extTemperature != null || profile.status.extHumidity != null
                 || profile.status.extVoltage != null || profile.status.extAnalogInput != null) {
             logger.debug("{}: Shelly Add-On detected with at least 1 external sensor", thingName);
@@ -1059,15 +1048,15 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
                 // no fw version available (e.g. BLU device)
                 return;
             }
-
             ShellyVersionDTO version = new ShellyVersionDTO();
             if (version.checkBeta(getString(prf.fwVersion))) {
-                logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate));
+                logger.info("{}: {}", prf.device.hostname,
+                        messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate));
             } else {
                 String minVersion = !gen2 ? SHELLY_API_MIN_FWVERSION : SHELLY2_API_MIN_FWVERSION;
                 if (version.compare(prf.fwVersion, minVersion) < 0) {
-                    logger.warn("{}: {}", prf.hostname,
-                            messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, minVersion));
+                    logger.warn("{}: {}", prf.device.hostname,
+                            messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate));
                 }
             }
             if (!gen2 && bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
@@ -1450,10 +1439,10 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
         Map<String, Object> properties = new TreeMap<>();
         properties.put(PROPERTY_VENDOR, VENDOR);
         if (profile.isInitialized()) {
-            properties.put(PROPERTY_MODEL_ID, getString(profile.settings.device.type));
-            properties.put(PROPERTY_MAC_ADDRESS, profile.mac);
+            properties.put(PROPERTY_MODEL_ID, getString(profile.device.type));
+            properties.put(PROPERTY_MAC_ADDRESS, profile.device.mac);
             properties.put(PROPERTY_FIRMWARE_VERSION, profile.fwVersion + "/" + profile.fwDate);
-            properties.put(PROPERTY_DEV_MODE, profile.mode);
+            properties.put(PROPERTY_DEV_MODE, profile.device.mode);
             if (profile.hasRelays) {
                 properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays));
                 properties.put(PROPERTY_NUM_ROLLERS, String.valueOf(profile.numRollers));
@@ -1481,7 +1470,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
         try {
             refreshSettings |= forceRefresh;
             if (refreshSettings) {
-                profile = api.getDeviceProfile(thingType);
+                profile = api.getDeviceProfile(thingType, null);
                 if (!isThingOnline()) {
                     logger.debug("{}: Device profile re-initialized (thingType={})", thingName, thingType);
                 }
index 8fc49526f68b033fcc549cbae14d5b85a2de30a4..b9a901612bb6d1352226f6b03896aa4ec4ca0ba3 100644 (file)
@@ -82,7 +82,7 @@ public class ShellyLightHandler extends ShellyBaseHandler {
 
         try {
             ShellyColorUtils oldCol = getCurrentColors(lightId);
-            oldCol.mode = profile.mode;
+            oldCol.mode = profile.device.mode;
             ShellyColorUtils col = new ShellyColorUtils(oldCol);
 
             boolean update = true;
@@ -317,7 +317,7 @@ public class ShellyLightHandler extends ShellyBaseHandler {
         }
 
         ShellyStatusLight status = api.getLightStatus();
-        logger.trace("{}: Updating light status in {} mode, {} channel(s)", thingName, profile.mode,
+        logger.trace("{}: Updating light status in {} mode, {} channel(s)", thingName, profile.device.mode,
                 status.lights.size());
 
         // In white mode we have multiple channels
index ef47191441e1cab1f939443560bcf662a1377ca2..6f6abdbd0fa7d0f63b54fbd85aa4123544f75774 100644 (file)
@@ -377,7 +377,7 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
 
         list.put(ACTION_RES_STATS, "Reset Statistics");
         list.put(ACTION_RESTART, "Reboot Device");
-        if (gen2) {
+        if (!gen2 || !profile.isBlu) {
             list.put(ACTION_PROTECT, "Protect Device");
         }
 
index ed7efa891cbdb535beae16a68f8bc0a881a043e3..d4ce439f1dcf042853a5a9911b6428da0a508aad 100644 (file)
@@ -87,7 +87,7 @@ public class ShellyManagerOtaPage extends ShellyManagerPage {
             String deviceType = getDeviceType(properties);
 
             String uri = !url.isEmpty() && connection.equals(CONNECTION_TYPE_CUSTOM) ? url
-                    : getFirmwareUrl(config.deviceIp, deviceType, profile.mode, version,
+                    : getFirmwareUrl(config.deviceIp, deviceType, profile.device.mode, version,
                             connection.equals(CONNECTION_TYPE_LOCAL));
             if (connection.equalsIgnoreCase(CONNECTION_TYPE_INTERNET)) {
                 // If target
@@ -100,7 +100,8 @@ public class ShellyManagerOtaPage extends ShellyManagerPage {
                 }
             } else if (connection.equalsIgnoreCase(CONNECTION_TYPE_LOCAL)) {
                 // redirect to local server -> http://<oh-ip>:<oh-port>/shelly/manager/ota?deviceType=xxx&version=xxx
-                String modeParm = !profile.mode.isEmpty() ? "&" + URLPARM_DEVMODE + "=" + profile.mode : "";
+                String modeParm = !profile.device.mode.isEmpty() ? "&" + URLPARM_DEVMODE + "=" + profile.device.mode
+                        : "";
                 url = URLPARM_URL + "=http://" + localIp + ":" + localPort + SHELLY_MGR_OTA_URI + urlEncode(
                         "?" + URLPARM_DEVTYPE + "=" + deviceType + modeParm + "&" + URLPARM_VERSION + "=" + version);
             } else if (connection.equalsIgnoreCase(CONNECTION_TYPE_CUSTOM)) {
index ddc696e80543a8e9c30b8fad808abf7404f5598f..7c0bdd0218d84e1dc3345c8ab23b09570ed3d3a1 100644 (file)
@@ -143,7 +143,7 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
         try {
             if (!profile.isGen2) { // currently there is no public firmware repo for Gen2
                 logger.debug("{}: Load firmware version list for device type {}", LOG_PREFIX, deviceType);
-                FwRepoEntry fw = getFirmwareRepoEntry(deviceType, profile.mode);
+                FwRepoEntry fw = getFirmwareRepoEntry(deviceType, profile.device.mode);
 
                 pVersion = extractFwVersion(fw.version);
                 bVersion = extractFwVersion(fw.betaVer);
@@ -238,7 +238,9 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
                 // return handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE) == OnOffType.ON;
                 return getBool(profile.status.hasUpdate);
             case FILTER_UNPROTECTED:
-                return !profile.auth;
+                if (profile.device.auth != null) {
+                    return !profile.device.auth;
+                }
             case "*":
             default:
                 return true;
index a70b4472d9f8df1c159373e3c826a759d39ffa49..5d7e46bfa1dbf6b04ebcdf35ab38c4cfb3e0caf0 100644 (file)
@@ -254,7 +254,8 @@ public class ShellyManagerPage {
         properties.put(ATTRIBUTE_APR_TRESHOLD,
                 profile.settings.apRoaming != null ? getOption(profile.settings.apRoaming.threshold) : "n/a");
         properties.put(ATTRIBUTE_PWD_PROTECT,
-                profile.auth ? "enabled, user=" + getString(profile.settings.login.username) : "disabled");
+                getBool(profile.device.auth) ? "enabled, user=" + getString(profile.settings.login.username)
+                        : "disabled");
         String tz = getString(profile.settings.timezone);
         properties.put(ATTRIBUTE_TIMEZONE,
                 (tz.isEmpty() ? "n/a" : tz) + ", auto-detect: " + getBool(profile.settings.tzautodetect));