]> git.basschouten.com Git - openhab-addons.git/commitdiff
[shelly] Fix Plus/Pro Auth support (#15284)
authorMarkus Michels <markus7017@gmail.com>
Thu, 27 Jul 2023 13:41:32 +0000 (15:41 +0200)
committerGitHub <noreply@github.com>
Thu, 27 Jul 2023 13:41:32 +0000 (15:41 +0200)
* Reimplement Auth Digist to return response as http header field instead
of being part of post data; avoid sending Basic Auth for Gen2 and
/shelly

Signed-off-by: Markus Michels <markus7017@gmail.com>
bundles/org.openhab.binding.shelly/README.md
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.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/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/Shelly2RpcSocket.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/thing/shellyGen2_relay.xml

index 982544d7a30f0ffa50aa4c66800e26dfc5543059..f7daae283a2abdbcc5a4d81035572ccbe56928fd 100644 (file)
@@ -997,7 +997,6 @@ You should calibrate the valve using the device Web UI or Shelly App before star
 | ------- | --------------- | -------- | --------- | ------------------------------------------------------------------- |
 | sensors | temperature     | Number   | yes       | Current Temperature in °C                                           |
 |         | state           | Contact  | yes       | Valve status: OPEN or CLOSED (position = 0)                         |
-|         | open            | Contact  | yes       | ON: "window is open" was detected, OFF: window is closed            |
 |         | lastUpdate      | DateTime | yes       | Timestamp of the last update (any sensor value changed)             |
 | control | targetTemp      | Number   | no        | Temperature in °C: 4=Low/Min; 5..30=target temperature;31=Hi/Max    |
 |         | position        | Dimmer   | no        | Set valve to manual mode (0..100%) disables auto-temp)              |
index 3a45219765f5c63098a32b4c308163d23d26960f..e3300b7766b559fc7acd0616dab7a6898413e7f3 100755 (executable)
@@ -291,7 +291,7 @@ public class ShellyBindingConstants {
     public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+
     public static final String SHELLY_API_FWCOIOT2 = "v1.8";// CoAP 2 with FW 1.8+
     public static final String SHELLY_API_FW_110 = "v1.10"; // FW 1.10 or newer detected, activates some add feature
-    public static final String SHELLY2_API_MIN_FWVERSION = "v0.10.2"; // Gen 2 minimum FW
+    public static final String SHELLY2_API_MIN_FWVERSION = "v0.10.1"; // Gen 2 minimum FW
 
     // Alarm types/messages
     public static final String ALARM_TYPE_NONE = "NONE";
@@ -327,7 +327,7 @@ public class ShellyBindingConstants {
     public static final int DIGITS_LUX = 0;
     public static final int DIGITS_PERCENT = 1;
 
-    public static final int SHELLY_API_TIMEOUT_MS = 15000;
+    public static final int SHELLY_API_TIMEOUT_MS = 10000;
     public static final int UPDATE_STATUS_INTERVAL_SECONDS = 3; // check for updates every x sec
     public static final int UPDATE_SKIP_COUNT = 20; // update every x triggers or when a key was pressed
     public static final int UPDATE_MIN_DELAY = 15;// update every x triggers or when a key was pressed
index f6b7dbd209fa9182cceb7abd5d47b3300ffb8954..1413cf9c608d26b46422d829db5773d4d7fa96a2 100644 (file)
@@ -33,7 +33,7 @@ public class ShellyApiResult {
     public String response = "";
     public int httpCode = -1;
     public String httpReason = "";
-    public String authResponse = "";
+    public String authChallenge = "";
 
     public ShellyApiResult() {
     }
index 8b97f9ae36330ba8c8f31a7dd4cd77d43df4e91f..e82dc8bee235829819a19c610233b0924ff6eac8 100644 (file)
@@ -14,16 +14,21 @@ package org.openhab.binding.shelly.internal.api;
 
 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS;
 import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 
 import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
 import java.util.Base64;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
+import javax.ws.rs.core.HttpHeaders;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.api.Request;
@@ -32,6 +37,8 @@ import org.eclipse.jetty.http.HttpFields;
 import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthChallenge;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRsp;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
@@ -49,8 +56,9 @@ import com.google.gson.Gson;
 public class ShellyHttpClient {
     private final Logger logger = LoggerFactory.getLogger(ShellyHttpClient.class);
 
-    public static final String HTTP_HEADER_AUTH = "Authorization";
+    public static final String HTTP_HEADER_AUTH = HttpHeaders.AUTHORIZATION;
     public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
+    public static final String HTTP_AUTH_TYPE_DIGEST = "Digest";
     public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
     public static final String CONTENT_TYPE_FORM_URLENC = "application/x-www-form-urlencoded";
 
@@ -72,6 +80,7 @@ public class ShellyHttpClient {
         this.thingName = thingName;
         setConfig(thingName, config);
         this.httpClient = httpClient;
+        this.httpClient.setConnectTimeout(SHELLY_API_TIMEOUT_MS);
     }
 
     public void initialize() throws ShellyApiException {
@@ -103,7 +112,7 @@ public class ShellyHttpClient {
         boolean timeout = false;
         while (retries > 0) {
             try {
-                apiResult = innerRequest(HttpMethod.GET, uri, "");
+                apiResult = innerRequest(HttpMethod.GET, uri, null, "");
                 if (timeout) {
                     logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
                             apiResult.getUrl());
@@ -128,10 +137,15 @@ public class ShellyHttpClient {
     }
 
     public String httpPost(String uri, String data) throws ShellyApiException {
-        return innerRequest(HttpMethod.POST, uri, data).response;
+        return innerRequest(HttpMethod.POST, uri, null, data).response;
+    }
+
+    public String httpPost(@Nullable Shelly2AuthChallenge auth, String data) throws ShellyApiException {
+        return innerRequest(HttpMethod.POST, SHELLYRPC_ENDPOINT, auth, data).response;
     }
 
-    private ShellyApiResult innerRequest(HttpMethod method, String uri, String data) throws ShellyApiException {
+    private ShellyApiResult innerRequest(HttpMethod method, String uri, @Nullable Shelly2AuthChallenge auth,
+            String data) throws ShellyApiException {
         Request request = null;
         String url = "http://" + config.deviceIp + uri;
         ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
@@ -140,10 +154,24 @@ public class ShellyHttpClient {
             request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
                     TimeUnit.MILLISECONDS);
 
-            if (!config.password.isEmpty() && !getString(data).contains("\"auth\":{")) {
-                String value = config.userId + ":" + config.password;
-                request.header(HTTP_HEADER_AUTH,
-                        HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
+            if (!uri.equals(SHELLY_URL_DEVINFO) && !config.password.isEmpty()) { // not for /shelly or no password
+                                                                                 // configured
+                // Add Auth info
+                // Gen 1: Basic Auth
+                // Gen 2: Digest Auth
+                String authHeader = "";
+                if (auth != null) { // only if we received an Auth challenge
+                    authHeader = formatAuthResponse(uri,
+                            buildAuthResponse(uri, auth, SHELLY2_AUTHDEF_USER, config.password));
+                } else {
+                    if (!uri.equals(SHELLYRPC_ENDPOINT)) {
+                        String bearer = config.userId + ":" + config.password;
+                        authHeader = HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(bearer.getBytes());
+                    }
+                }
+                if (!authHeader.isEmpty()) {
+                    request.header(HTTP_HEADER_AUTH, authHeader);
+                }
             }
             fillPostData(request, data);
             logger.trace("{}: HTTP {} for {} {}\n{}", thingName, method, url, data, request.getHeaders());
@@ -162,14 +190,14 @@ public class ShellyHttpClient {
                     apiResult.httpCode = message.error.code;
                     apiResult.response = message.error.message;
                     if (getInteger(message.error.code) == HttpStatus.UNAUTHORIZED_401) {
-                        apiResult.authResponse = getString(message.error.message).replaceAll("\\\"", "\"");
+                        apiResult.authChallenge = getString(message.error.message).replaceAll("\\\"", "\"");
                     }
                 }
             }
             HttpFields headers = contentResponse.getHeaders();
-            String auth = headers.get(HttpHeader.WWW_AUTHENTICATE);
-            if (!getString(auth).isEmpty()) {
-                apiResult.authResponse = auth;
+            String authChallenge = headers.get(HttpHeader.WWW_AUTHENTICATE);
+            if (!getString(authChallenge).isEmpty()) {
+                apiResult.authChallenge = authChallenge;
             }
 
             // validate response, API errors are reported as Json
@@ -191,6 +219,36 @@ public class ShellyHttpClient {
         return apiResult;
     }
 
+    protected @Nullable Shelly2AuthRsp buildAuthResponse(String uri, @Nullable Shelly2AuthChallenge challenge,
+            String user, String password) throws ShellyApiException {
+        if (challenge == null) {
+            return null; // not required
+        }
+        if (!SHELLY2_AUTHTTYPE_DIGEST.equalsIgnoreCase(challenge.authType)
+                || !SHELLY2_AUTHALG_SHA256.equalsIgnoreCase(challenge.algorithm)) {
+            throw new IllegalArgumentException("Unsupported Auth type/algorithm requested by device");
+        }
+        Shelly2AuthRsp response = new Shelly2AuthRsp();
+        response.username = user;
+        response.realm = challenge.realm;
+        response.nonce = challenge.nonce;
+        response.cnonce = Long.toHexString((long) Math.floor(Math.random() * 10e8));
+        response.nc = "00000001";
+        response.authType = challenge.authType;
+        response.algorithm = challenge.algorithm;
+        String ha1 = sha256(response.username + ":" + response.realm + ":" + password);
+        String ha2 = sha256(HttpMethod.POST + ":" + uri);// SHELLY2_AUTH_NOISE;
+        response.response = sha256(
+                ha1 + ":" + response.nonce + ":" + response.nc + ":" + response.cnonce + ":" + "auth" + ":" + ha2);
+        return response;
+    }
+
+    protected String formatAuthResponse(String uri, @Nullable Shelly2AuthRsp rsp) {
+        return rsp != null ? MessageFormat.format(HTTP_AUTH_TYPE_DIGEST
+                + " username=\"{0}\", realm=\"{1}\", uri=\"{2}\", nonce=\"{3}\", cnonce=\"{4}\", nc=\"{5}\", qop=\"auth\",response=\"{6}\", algorithm=\"{7}\", ",
+                rsp.username, rsp.realm, uri, rsp.nonce, rsp.cnonce, rsp.nc, rsp.response, rsp.algorithm) : "";
+    }
+
     /**
      * Fill in POST data, set http headers
      *
index 5470c8c6b51ebba72d2cc98bc1aaa1e353364008..afb0482ad5eb968510f03f8d773d00f10f07d183 100644 (file)
@@ -52,8 +52,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSe
 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorBat;
 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorHum;
 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorLux;
-import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRequest;
-import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthResponse;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRsp;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigCover;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigInput;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigSwitch;
@@ -92,7 +91,7 @@ public class Shelly2ApiClient extends ShellyHttpClient {
     protected final ShellyStatusSensor sensorData = new ShellyStatusSensor();
     protected final ArrayList<ShellyRollerStatus> rollerStatus = new ArrayList<>();
     protected @Nullable ShellyThingInterface thing;
-    protected @Nullable Shelly2AuthRequest authReq;
+    protected @Nullable Shelly2AuthRsp authReq;
 
     public Shelly2ApiClient(String thingName, ShellyThingInterface thing) {
         super(thingName, thing);
@@ -793,23 +792,6 @@ public class Shelly2ApiClient extends ShellyHttpClient {
         return request;
     }
 
-    protected Shelly2AuthRequest buildAuthRequest(Shelly2AuthResponse authParm, String user, String realm,
-            String password) throws ShellyApiException {
-        Shelly2AuthRequest authReq = new Shelly2AuthRequest();
-        authReq.username = "admin";
-        authReq.realm = realm;
-        authReq.nonce = authParm.nonce;
-        authReq.cnonce = (long) Math.floor(Math.random() * 10e8);
-        authReq.nc = authParm.nc != null ? authParm.nc : 1;
-        authReq.authType = SHELLY2_AUTHTTYPE_DIGEST;
-        authReq.algorithm = SHELLY2_AUTHALG_SHA256;
-        String ha1 = sha256(authReq.username + ":" + authReq.realm + ":" + password);
-        String ha2 = SHELLY2_AUTH_NOISE;
-        authReq.response = sha256(
-                ha1 + ":" + authReq.nonce + ":" + authReq.nc + ":" + authReq.cnonce + ":" + "auth" + ":" + ha2);
-        return authReq;
-    }
-
     protected String mapValue(Map<String, String> map, @Nullable String key) {
         String value;
         boolean known = key != null && !key.isEmpty() && map.containsKey(key);
index 38f9940616c6d9295e79e563fcff0586f7b012d6..672f9e3b8cc962a52d5957a8f064e935a80d0ee4 100644 (file)
@@ -27,6 +27,8 @@ import com.google.gson.annotations.SerializedName;
  * @author Markus Michels - Initial contribution
  */
 public class Shelly2ApiJsonDTO {
+    public static final String SHELLYRPC_ENDPOINT = "/rpc";
+
     public static final String SHELLYRPC_METHOD_CLASS_SHELLY = "Shelly";
     public static final String SHELLYRPC_METHOD_CLASS_SWITCH = "Switch";
 
@@ -1004,7 +1006,7 @@ public class Shelly2ApiJsonDTO {
         public Object params;
         public String event;
         public Object result;
-        public Shelly2AuthRequest auth;
+        public Shelly2AuthRsp auth;
         public Shelly2RpcMessageError error;
     }
 
@@ -1022,31 +1024,32 @@ public class Shelly2ApiJsonDTO {
         public Shelly2RpcMessageError error;
     }
 
+    public static String SHELLY2_AUTHDEF_USER = "admin";
     public static String SHELLY2_AUTHTTYPE_DIGEST = "digest";
     public static String SHELLY2_AUTHTTYPE_STRING = "string";
     public static String SHELLY2_AUTHALG_SHA256 = "SHA-256";
     // = ':auth:'+HexHash("dummy_method:dummy_uri");
     public static String SHELLY2_AUTH_NOISE = "6370ec69915103833b5222b368555393393f098bfbfbb59f47e0590af135f062";
 
-    public static class Shelly2AuthRequest {
-        public String username;
-        public Long nonce;
-        public Long cnonce;
-        public Integer nc;
-        public String realm;
-        public String algorithm;
-        public String response;
+    public static class Shelly2AuthChallenge { // on 401 message contains the auth info
         @SerializedName("auth_type")
         public String authType;
+        public String nonce;
+        public String nc;
+        public String realm;
+        public String algorithm;
     }
 
-    public static class Shelly2AuthResponse { // on 401 message contains the auth info
-        @SerializedName("auth_type")
-        public String authType;
-        public Long nonce;
-        public Integer nc;
+    public static class Shelly2AuthRsp {
+        public String username;
+        public String nonce;
+        public String cnonce;
+        public String nc;
         public String realm;
         public String algorithm;
+        public String response;
+        @SerializedName("auth_type")
+        public String authType;
     }
 
     // BTHome samples
index 33ca863ee3e803caf0be88b0c3186fcc846950e7..de378fedab84446a6c031a80fbbd9abb90aabeab 100644 (file)
@@ -55,7 +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.Shelly2AuthResponse;
+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;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
@@ -81,6 +81,7 @@ import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptRe
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
 import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
+import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
 import org.openhab.core.library.unit.SIUnits;
 import org.openhab.core.thing.ThingStatusDetail;
 import org.slf4j.Logger;
@@ -99,7 +100,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
     protected boolean initialized = false;
     private boolean discovery = false;
     private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
-    private Shelly2AuthResponse authInfo = new Shelly2AuthResponse();
+    private @Nullable Shelly2AuthChallenge authInfo;
 
     /**
      * Regular constructor - called by Thing handler
@@ -203,6 +204,11 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
 
         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, "/");
+        }
+
         profile.hostname = device.hostname;
         profile.deviceType = device.type;
         profile.mac = device.mac;
@@ -388,6 +394,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
                 if (ourId != -1) {
                     startScript(ourId, false);
                     enableScript(script, false);
+                    deleteScript(ourId);
                     logger.debug("{}: Script {} was disabledd, id={}", thingName, script, ourId);
                 }
                 return;
@@ -439,8 +446,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
             }
             if (upload && ourId != -1) {
                 // Delete existing script
-                logger.debug("{}: Delete existing script", thingName);
-                apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_DELETE).withId(ourId));
+                deleteScript(ourId);
             }
 
             if (upload) {
@@ -479,10 +485,8 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
 
             if (!running) {
                 running = startScript(ourId, true);
-            }
-            if (!discovery) {
-                logger.info("{}: Script {} {}", thingName, script,
-                        running ? "was successfully (re)started" : "failed to start");
+                logger.debug("{}: Script {} {}", thingName, script,
+                        running ? "was successfully started" : "failed to start");
             }
         } catch (ShellyApiException e) {
             ShellyApiResult res = e.getApiResult();
@@ -515,10 +519,25 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
             apiRequest(SHELLYRPC_METHOD_SCRIPT_SETCONFIG, params, String.class);
             return true;
         } catch (ShellyApiException e) {
+            logger.debug("{}: Unable to enable script {}", thingName, script, e);
             return false;
         }
     }
 
+    private boolean deleteScript(int id) {
+        if (id == -1) {
+            throw new IllegalArgumentException("Invalid Script Id");
+        }
+        try {
+            logger.debug("{}: Delete existing script with id{}", thingName, id);
+            apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_DELETE).withId(id));
+            return true;
+        } catch (ShellyApiException e) {
+            logger.debug("{}: Unable to delete script with id {}", thingName, id);
+        }
+        return false;
+    }
+
     @Override
     public void onConnect(String deviceIp, boolean connected) {
         if (thing == null && thingTable != null) {
@@ -550,7 +569,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
             if (message.error != null) {
                 if (message.error.code == HttpStatus.UNAUTHORIZED_401 && !getString(message.error.message).isEmpty()) {
                     // Save nonce for notification
-                    Shelly2AuthResponse auth = gson.fromJson(message.error.message, Shelly2AuthResponse.class);
+                    Shelly2AuthChallenge auth = gson.fromJson(message.error.message, Shelly2AuthChallenge.class);
                     if (auth != null && auth.realm == null) {
                         logger.debug("{}: Authentication data received: {}", thingName, message.error.message);
                         authInfo = auth;
@@ -757,13 +776,17 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
             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.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.hasUpdate = new ShellyVersionDTO().compare(profile.fwVersion, status.update.betaVersion) < 0;
             }
         }
 
-        if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null) {
+        if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null)
+
+        {
             List<Object> values = new ArrayList<>();
             String boot = getString(ds.sys.wakeUpReason.boot);
             String cause = getString(ds.sys.wakeUpReason.cause);
@@ -793,7 +816,6 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
         return relayStatus;
     }
 
-    @SuppressWarnings("null")
     @Override
     public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
         ShellyDeviceProfile profile = getProfile();
@@ -1128,35 +1150,32 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
         Shelly2RpcBaseMessage req = buildRequest(method, params);
         try {
             reconnect(); // make sure WS is connected
-
-            if (authInfo.realm != null) {
-                req.auth = buildAuthRequest(authInfo, config.userId, config.serviceName, config.password);
-            }
             json = rpcPost(gson.toJson(req));
         } catch (ShellyApiException e) {
             ShellyApiResult res = e.getApiResult();
-            String auth = getString(res.authResponse);
+            String auth = getString(res.authChallenge);
             if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) {
                 String[] options = auth.split(",");
+                authInfo = new Shelly2AuthChallenge();
                 for (String o : options) {
                     String key = substringBefore(o, "=").stripLeading().trim();
                     String value = substringAfter(o, "=").replaceAll("\"", "").trim();
                     switch (key) {
                         case "Digest qop":
+                            authInfo.authType = SHELLY2_AUTHTTYPE_DIGEST;
                             break;
                         case "realm":
                             authInfo.realm = value;
                             break;
                         case "nonce":
-                            authInfo.nonce = Long.parseLong(value, 16);
+                            // authInfo.nonce = Long.parseLong(value, 16);
+                            authInfo.nonce = value;
                             break;
                         case "algorithm":
                             authInfo.algorithm = value;
                             break;
                     }
                 }
-                authInfo.nc = 1;
-                req.auth = buildAuthRequest(authInfo, config.userId, authInfo.realm, config.password);
                 json = rpcPost(gson.toJson(req));
             } else {
                 throw e;
@@ -1187,7 +1206,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
     }
 
     private String rpcPost(String postData) throws ShellyApiException {
-        return httpPost("/rpc", postData);
+        return httpPost(authInfo, postData);
     }
 
     private void reconnect() throws ShellyApiException {
index ecf724ff0801e2ecd23cc8871b0c840cb25390c6..a043772cd9a6ec29a6bf73c6889207639ed24341 100644 (file)
@@ -114,7 +114,7 @@ public class Shelly2RpcSocket {
         try {
             disconnect(); // for safety
 
-            URI uri = new URI("ws://" + deviceIp + "/rpc");
+            URI uri = new URI("ws://" + deviceIp + SHELLYRPC_ENDPOINT);
             ClientUpgradeRequest request = new ClientUpgradeRequest();
             request.setHeader(HttpHeaders.HOST, deviceIp);
             request.setHeader("Origin", "http://" + deviceIp);
@@ -276,6 +276,8 @@ public class Shelly2RpcSocket {
                                                     e.event, e.data.name);
                                         }
                                     }
+                                } else {
+                                    handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
                                 }
                             }
                         }
index dd808628826d79fd6c30e702244108753a7a9306..959c845e014f166083a2f90be19a84fa843d0f22 100755 (executable)
@@ -104,7 +104,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
     private final ShellyChannelCache cache;
     private final int cacheCount = UPDATE_SETTINGS_INTERVAL_SECONDS / UPDATE_STATUS_INTERVAL_SECONDS;
 
-    private final boolean gen2;
+    private boolean gen2 = false;
     private final boolean blu;
     protected boolean autoCoIoT = false;
 
@@ -1138,11 +1138,12 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
      * @param mode Device mode (e.g. relay, roller)
      */
     protected void changeThingType(String thingType, String mode) {
-        ThingTypeUID thingTypeUID = ShellyThingCreator.getThingTypeUID(thingType, "", mode);
+        String deviceType = substringBefore(thingType, "-");
+        ThingTypeUID thingTypeUID = ShellyThingCreator.getThingTypeUID(thingType, deviceType, mode);
         if (!thingTypeUID.equals(THING_TYPE_SHELLYUNKNOWN)) {
             logger.debug("{}: Changing thing type to {}", getThing().getLabel(), thingTypeUID);
             Map<String, String> properties = editProperties();
-            properties.replace(PROPERTY_DEV_TYPE, thingType);
+            properties.replace(PROPERTY_DEV_TYPE, deviceType);
             properties.replace(PROPERTY_DEV_MODE, mode);
             updateProperties(properties);
             changeThingType(thingTypeUID, getConfig());
index 9e75954a76a9451845437f1b114993ba41b56764..b66b8137fc01efef2a56a233b384da3584c70d3d 100644 (file)
                <label>ShellyPlus 2 Relay</label>
                <description>@text/thing-type.shelly.shellyplus2-relay.description</description>
                <channel-groups>
-                       <channel-group id="relay1" typeId="relayChannel"/>
-                       <channel-group id="meter1" typeId="meter"/>
-                       <channel-group id="relay2" typeId="relayChannel"/>
-                       <channel-group id="meter2" typeId="meter"/>
+                       <channel-group id="relay1" typeId="relayChannel">
+                               <label>@text/channel-group-type.shelly.relayChannel1.label</label>
+                       </channel-group>
+                       <channel-group id="meter1" typeId="meter">
+                               <label>@text/channel-group-type.shelly.meter1.label</label>
+                       </channel-group>
+                       <channel-group id="relay2" typeId="relayChannel">
+                               <label>@text/channel-group-type.shelly.relayChannel2.label</label>
+                       </channel-group>
+                       <channel-group id="meter2" typeId="meter">
+                               <label>@text/channel-group-type.shelly.meter1.label</label>
+                       </channel-group>
                        <channel-group id="device" typeId="deviceStatus"/>
                </channel-groups>