]> git.basschouten.com Git - openhab-addons.git/commitdiff
[ecovacs] Add support for new API for fetching cleaning logs (#16524)
authormaniac103 <dannybaumann@web.de>
Sun, 17 Mar 2024 19:47:36 +0000 (20:47 +0100)
committerGitHub <noreply@github.com>
Sun, 17 Mar 2024 19:47:36 +0000 (20:47 +0100)
The existing cleaning logs API is only populated for devices older than
the T9/N9 generation; all newer devices use a new API. Since the new API
isn't populated for older devices, select the correct API depending on
device type.

Signed-off-by: Danny Baumann <dannybaumann@web.de>
16 files changed:
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsBindingConstants.java
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApiConfiguration.java
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsDevice.java
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiImpl.java
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiUrlFactory.java
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsIotMqDevice.java
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsXmppDevice.java
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanLogRecord.java [new file with mode: 0644]
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanLogsResponse.java
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanResultsResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/DeviceCapability.java
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/HashUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/MD5Util.java [deleted file]
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/handler/EcovacsApiHandler.java
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/handler/EcovacsVacuumHandler.java
bundles/org.openhab.binding.ecovacs/src/main/resources/devices/supported_device_list.json

index 3c85b30728bd45ad5a0bdc24e8214d4753ccff2f..1c4dd4249b965bd47aa8ada5e369cbd9dd649b5c 100644 (file)
@@ -37,6 +37,7 @@ public class EcovacsBindingConstants {
     public static final String CLIENT_SECRET = "6c319b2a5cd3e66e39159c2e28f2fce9";
     public static final String AUTH_CLIENT_KEY = "1520391491841";
     public static final String AUTH_CLIENT_SECRET = "77ef58ce3afbe337da74aa8c5ab963a9";
+    public static final String APP_KEY = "2ea31cf06e6711eaa0aff7b9558a534e";
 
     // List of all Thing Type UIDs
     public static final ThingTypeUID THING_TYPE_API = new ThingTypeUID(BINDING_ID, "ecovacsapi");
index 8208b3a5f11912fcc1181ae0f6d0dd385fa66408..89b959a182dc123a3c4f2611afc73c69e27fae0b 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.binding.ecovacs.internal.api;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.ecovacs.internal.api.util.MD5Util;
+import org.openhab.binding.ecovacs.internal.api.util.HashUtil;
 
 /**
  * @author Johannes Ptaszyk - Initial contribution
@@ -30,10 +30,12 @@ public final class EcovacsApiConfiguration {
     private final String clientSecret;
     private final String authClientKey;
     private final String authClientSecret;
+    private final String appKey;
 
     public EcovacsApiConfiguration(String deviceId, String username, String password, String continent, String country,
-            String language, String clientKey, String clientSecret, String authClientKey, String authClientSecret) {
-        this.deviceId = MD5Util.getMD5Hash(deviceId);
+            String language, String clientKey, String clientSecret, String authClientKey, String authClientSecret,
+            String appKey) {
+        this.deviceId = HashUtil.getMD5Hash(deviceId);
         this.username = username;
         this.password = password;
         this.continent = continent;
@@ -43,6 +45,7 @@ public final class EcovacsApiConfiguration {
         this.clientSecret = clientSecret;
         this.authClientKey = authClientKey;
         this.authClientSecret = authClientSecret;
+        this.appKey = appKey;
     }
 
     public String getDeviceId() {
@@ -90,7 +93,7 @@ public final class EcovacsApiConfiguration {
         return "ecouser.net";
     }
 
-    public String getPortalAUthRequestWith() {
+    public String getPortalAuthRequestWith() {
         return "users";
     }
 
@@ -110,12 +113,28 @@ public final class EcovacsApiConfiguration {
         return "google_play";
     }
 
+    public String getAppId() {
+        return "ecovacs";
+    }
+
+    public String getAppPlatform() {
+        return "android";
+    }
+
     public String getAppCode() {
         return "global_e";
     }
 
     public String getAppVersion() {
-        return "1.6.3";
+        return "2.3.7";
+    }
+
+    public String getAppKey() {
+        return appKey;
+    }
+
+    public String getAppUserAgent() {
+        return "EcovacsHome/2.3.7 (Linux; U; Android 5.1.1; A5010 Build/LMY48Z)";
     }
 
     public String getDeviceType() {
index 4bee37a2ef3351a538e1eb846ca2881450fed3d0..bda227ed70148e3eee87afae0ba9fd800f1a20d1 100644 (file)
@@ -59,4 +59,6 @@ public interface EcovacsDevice {
     <T> T sendCommand(IotDeviceCommand<T> command) throws EcovacsApiException, InterruptedException;
 
     List<CleanLogRecord> getCleanLogs() throws EcovacsApiException, InterruptedException;
+
+    Optional<byte[]> downloadCleanMapImage(CleanLogRecord record) throws EcovacsApiException, InterruptedException;
 }
index cfcf42fc34b24aa867882df5f8b2b33514f4a4e2..a3f1603f2c87fa0517ddc3237f4e52fc292e6009 100644 (file)
@@ -59,14 +59,16 @@ import org.openhab.binding.ecovacs.internal.api.impl.dto.response.main.ResponseW
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.Device;
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.IotProduct;
+import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalCleanLogRecord;
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalCleanLogsResponse;
+import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalCleanResultsResponse;
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalDeviceResponse;
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandJsonResponse;
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandXmlResponse;
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotProductResponse;
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalLoginResponse;
 import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
-import org.openhab.binding.ecovacs.internal.api.util.MD5Util;
+import org.openhab.binding.ecovacs.internal.api.util.HashUtil;
 import org.openhab.core.OpenHAB;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -116,8 +118,8 @@ public final class EcovacsApiImpl implements EcovacsApi {
     private AccessData login() throws EcovacsApiException, InterruptedException {
         HashMap<String, String> loginParameters = new HashMap<>();
         loginParameters.put("account", configuration.getUsername());
-        loginParameters.put("password", MD5Util.getMD5Hash(configuration.getPassword()));
-        loginParameters.put("requestId", MD5Util.getMD5Hash(String.valueOf(System.currentTimeMillis())));
+        loginParameters.put("password", HashUtil.getMD5Hash(configuration.getPassword()));
+        loginParameters.put("requestId", HashUtil.getMD5Hash(String.valueOf(System.currentTimeMillis())));
         loginParameters.put("authTimeZone", configuration.getTimeZone());
         loginParameters.put("country", configuration.getCountry());
         loginParameters.put("lang", configuration.getLanguage());
@@ -310,8 +312,7 @@ public final class EcovacsApiImpl implements EcovacsApi {
         }
     }
 
-    public List<PortalCleanLogsResponse.LogRecord> fetchCleanLogs(Device device)
-            throws EcovacsApiException, InterruptedException {
+    public List<PortalCleanLogRecord> fetchCleanLogs(Device device) throws EcovacsApiException, InterruptedException {
         PortalCleanLogsRequest data = new PortalCleanLogsRequest(createAuthData(), device.getDid(),
                 device.getResource());
         String url = EcovacsApiUrlFactory.getPortalLogUrl(configuration);
@@ -324,12 +325,39 @@ public final class EcovacsApiImpl implements EcovacsApi {
         return responseObj.records;
     }
 
+    public List<PortalCleanLogRecord> fetchCleanResultsLog(Device device)
+            throws EcovacsApiException, InterruptedException {
+        String url = EcovacsApiUrlFactory.getPortalCleanResultsLogUrl(configuration);
+        Request request = createSignedAppRequest(url).param("auth", gson.toJson(createAuthData())) //
+                .param("channel", configuration.getChannel()) //
+                .param("did", device.getDid()) //
+                .param("defaultLang", "EN") //
+                .param("logType", "clean") //
+                .param("res", device.getResource()) //
+                .param("size", "20") //
+                .param("version", "v2");
+
+        ContentResponse response = executeRequest(request);
+        PortalCleanResultsResponse responseObj = handleResponse(response, PortalCleanResultsResponse.class);
+        if (!responseObj.wasSuccessful()) {
+            throw new EcovacsApiException("Fetching clean results failed");
+        }
+        logger.trace("{}: Fetching cleaning results yields {} records", device.getName(), responseObj.records.size());
+        return responseObj.records;
+    }
+
+    public byte[] downloadCleanMapImage(String url, boolean useSigning)
+            throws EcovacsApiException, InterruptedException {
+        Request request = useSigning ? createSignedAppRequest(url) : httpClient.newRequest(url).method(HttpMethod.GET);
+        return executeRequest(request).getContent();
+    }
+
     private PortalAuthRequestParameter createAuthData() {
         PortalLoginResponse loginData = this.loginData;
         if (loginData == null) {
             throw new IllegalStateException("Not logged in");
         }
-        return new PortalAuthRequestParameter(configuration.getPortalAUthRequestWith(), loginData.getUserId(),
+        return new PortalAuthRequestParameter(configuration.getPortalAuthRequestWith(), loginData.getUserId(),
                 configuration.getRealm(), loginData.getToken(), configuration.getResource());
     }
 
@@ -371,7 +399,7 @@ public final class EcovacsApiImpl implements EcovacsApi {
         signOnText.append(clientSecret);
 
         signedRequestParameters.put("authAppkey", clientKey);
-        signedRequestParameters.put("authSign", MD5Util.getMD5Hash(signOnText.toString()));
+        signedRequestParameters.put("authSign", HashUtil.getMD5Hash(signOnText.toString()));
 
         Request request = httpClient.newRequest(url).method(HttpMethod.GET);
         signedRequestParameters.forEach(request::param);
@@ -379,6 +407,27 @@ public final class EcovacsApiImpl implements EcovacsApi {
         return request;
     }
 
+    private Request createSignedAppRequest(String url) {
+        String timestamp = Long.toString(System.currentTimeMillis());
+        String signContent = configuration.getAppId() + configuration.getAppKey() + timestamp;
+        PortalLoginResponse loginData = this.loginData;
+        if (loginData == null) {
+            throw new IllegalStateException("Not logged in");
+        }
+        return httpClient.newRequest(url).method(HttpMethod.GET)
+                .header("Authorization", "Bearer " + loginData.getToken()) //
+                .header("token", loginData.getToken()) //
+                .header("appid", configuration.getAppId()) //
+                .header("plat", configuration.getAppPlatform()) //
+                .header("userid", loginData.getUserId()) //
+                .header("user-agent", configuration.getAppUserAgent()) //
+                .header("v", configuration.getAppVersion()) //
+                .header("country", configuration.getCountry()) //
+                .header("sign", HashUtil.getSHA256Hash(signContent)) //
+                .header("signType", "sha256") //
+                .param("et1", timestamp);
+    }
+
     private Request createJsonRequest(String url, Object data) {
         return httpClient.newRequest(url).method(HttpMethod.POST).header(HttpHeader.CONTENT_TYPE, "application/json")
                 .content(new StringContentProvider(gson.toJson(data)));
index d50ab23340b9630a06533ac9234b5cb6af4dc368..98641b9b922000b046e2c5a47e4259df949fc12d 100644 (file)
@@ -27,10 +27,11 @@ public final class EcovacsApiUrlFactory {
 
     private static final String MAIN_URL_LOGIN_PATH = "/user/login";
 
-    private static final String PORTAL_USERS_PATH = "/users/user.do";
-    private static final String PORTAL_IOT_PRODUCT_PATH = "/pim/product/getProductIotMap";
-    private static final String PORTAL_IOT_DEVMANAGER_PATH = "/iot/devmanager.do";
-    private static final String PORTAL_LOG_PATH = "/lg/log.do";
+    private static final String PORTAL_USERS_PATH = "/api/users/user.do";
+    private static final String PORTAL_IOT_PRODUCT_PATH = "/api/pim/product/getProductIotMap";
+    private static final String PORTAL_IOT_DEVMANAGER_PATH = "/api/iot/devmanager.do";
+    private static final String PORTAL_LOG_PATH = "/api/lg/log.do";
+    private static final String PORTAL_CLEAN_RESULTS_PATH = "/app/dln/api/log/clean_result/list";
 
     public static String getLoginUrl(EcovacsApiConfiguration config) {
         return getMainUrl(config) + MAIN_URL_LOGIN_PATH;
@@ -57,9 +58,13 @@ public final class EcovacsApiUrlFactory {
         return getPortalUrl(config) + PORTAL_LOG_PATH;
     }
 
+    public static String getPortalCleanResultsLogUrl(EcovacsApiConfiguration config) {
+        return getPortalUrl(config) + PORTAL_CLEAN_RESULTS_PATH;
+    }
+
     private static String getPortalUrl(EcovacsApiConfiguration config) {
         String continentSuffix = "cn".equalsIgnoreCase(config.getCountry()) ? "" : "-" + config.getContinent();
-        return String.format("https://portal%1$s.ecouser.net/api", continentSuffix);
+        return String.format("https://portal%1$s.ecouser.net", continentSuffix);
     }
 
     private static String getMainUrl(EcovacsApiConfiguration config) {
index 1bc866d36e15cf10a8810ac92502fde0ff73ab14..18e1e493e5b47b081ba6ea5d3606f897b7800ac5 100644 (file)
@@ -34,6 +34,7 @@ import org.openhab.binding.ecovacs.internal.api.commands.GetCleanLogsCommand;
 import org.openhab.binding.ecovacs.internal.api.commands.GetFirmwareVersionCommand;
 import org.openhab.binding.ecovacs.internal.api.commands.IotDeviceCommand;
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.Device;
+import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalCleanLogRecord;
 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalLoginResponse;
 import org.openhab.binding.ecovacs.internal.api.model.CleanLogRecord;
 import org.openhab.binding.ecovacs.internal.api.model.DeviceCapability;
@@ -103,12 +104,25 @@ public class EcovacsIotMqDevice implements EcovacsDevice {
         if (desc.protoVersion == ProtocolVersion.XML) {
             logEntries = sendCommand(new GetCleanLogsCommand()).stream();
         } else {
-            logEntries = api.fetchCleanLogs(device).stream().map(record -> new CleanLogRecord(record.timestamp,
-                    record.duration, record.area, Optional.ofNullable(record.imageUrl), record.type));
+            List<PortalCleanLogRecord> log = hasCapability(DeviceCapability.USES_CLEAN_RESULTS_LOG_API)
+                    ? api.fetchCleanResultsLog(device)
+                    : api.fetchCleanLogs(device);
+            logEntries = log.stream().map(record -> new CleanLogRecord(record.timestamp, record.duration, record.area,
+                    Optional.ofNullable(record.imageUrl), record.type));
         }
         return logEntries.sorted((lhs, rhs) -> rhs.timestamp.compareTo(lhs.timestamp)).collect(Collectors.toList());
     }
 
+    @Override
+    public Optional<byte[]> downloadCleanMapImage(CleanLogRecord record)
+            throws EcovacsApiException, InterruptedException {
+        if (record.mapImageUrl.isEmpty()) {
+            return Optional.empty();
+        }
+        boolean needsSigning = hasCapability(DeviceCapability.USES_CLEAN_RESULTS_LOG_API);
+        return Optional.of(api.downloadCleanMapImage(record.mapImageUrl.get(), needsSigning));
+    }
+
     @Override
     public void connect(final EventListener listener, ScheduledExecutorService scheduler)
             throws EcovacsApiException, InterruptedException {
index c5c365d90683ccf9bda451ce146da47201d94ca3..992484c2fa8dde4854b619d9c01a03dace7a0070 100644 (file)
@@ -153,6 +153,12 @@ public class EcovacsXmppDevice implements EcovacsDevice {
         return sendCommand(new GetCleanLogsCommand());
     }
 
+    @Override
+    public Optional<byte[]> downloadCleanMapImage(CleanLogRecord record)
+            throws EcovacsApiException, InterruptedException {
+        return Optional.empty();
+    }
+
     @Override
     public void connect(final EventListener listener, final ScheduledExecutorService scheduler)
             throws EcovacsApiException {
diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanLogRecord.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanLogRecord.java
new file mode 100644 (file)
index 0000000..c90b62a
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * 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.ecovacs.internal.api.impl.dto.response.portal;
+
+import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Danny Baumann - Initial contribution
+ */
+public class PortalCleanLogRecord {
+    @SerializedName("ts")
+    public final long timestamp;
+
+    @SerializedName("last")
+    public final long duration;
+
+    public final int area;
+
+    public final String id;
+
+    public final String imageUrl;
+
+    public final CleanMode type;
+
+    // more possible fields:
+    // aiavoid (int), aitypes (list of something), aiopen (int), aq (int), mapName (string),
+    // sceneName (string), triggerMode (int), powerMopType (int), enablePowerMop (int), cornerDeep (int)
+
+    PortalCleanLogRecord(long timestamp, long duration, int area, String id, String imageUrl, CleanMode type) {
+        this.timestamp = timestamp;
+        this.duration = duration;
+        this.area = area;
+        this.id = id;
+        this.imageUrl = imageUrl;
+        this.type = type;
+    }
+}
index b39a46acb3b79ee8bf4b053bcfd9fb995f55bdee..b1f13d0013e861c3388b87ce55d1ba720e0754a2 100644 (file)
@@ -14,48 +14,19 @@ package org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal;
 
 import java.util.List;
 
-import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
-
 import com.google.gson.annotations.SerializedName;
 
 /**
  * @author Johannes Ptaszyk - Initial contribution
  */
 public class PortalCleanLogsResponse {
-    public static class LogRecord {
-        @SerializedName("ts")
-        public final long timestamp;
-
-        @SerializedName("last")
-        public final long duration;
-
-        public final int area;
-
-        public final String id;
-
-        public final String imageUrl;
-
-        public final CleanMode type;
-
-        // more possible fields: aiavoid (int), aitypes (list of something), stopReason (int)
-
-        LogRecord(long timestamp, long duration, int area, String id, String imageUrl, CleanMode type) {
-            this.timestamp = timestamp;
-            this.duration = duration;
-            this.area = area;
-            this.id = id;
-            this.imageUrl = imageUrl;
-            this.type = type;
-        }
-    }
-
     @SerializedName("logs")
-    public final List<LogRecord> records;
+    public final List<PortalCleanLogRecord> records;
 
     @SerializedName("ret")
     final String result;
 
-    PortalCleanLogsResponse(String result, List<LogRecord> records) {
+    PortalCleanLogsResponse(String result, List<PortalCleanLogRecord> records) {
         this.result = result;
         this.records = records;
     }
diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanResultsResponse.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanResultsResponse.java
new file mode 100644 (file)
index 0000000..969f6eb
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * 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.ecovacs.internal.api.impl.dto.response.portal;
+
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Danny Baumann - Initial contribution
+ */
+public class PortalCleanResultsResponse {
+    @SerializedName("data")
+    public final List<PortalCleanLogRecord> records;
+
+    final int code;
+    final String message;
+
+    PortalCleanResultsResponse(int code, String message, List<PortalCleanLogRecord> records) {
+        this.code = code;
+        this.message = message;
+        this.records = records;
+    }
+
+    public boolean wasSuccessful() {
+        return code == 0;
+    }
+}
index c9cea1422aa1d1fa538ba2c07df0b18a9141a3a7..262a3c2dbf107f8aec58267c4da23d328e46ddf1 100644 (file)
@@ -47,6 +47,8 @@ public enum DeviceCapability {
     TRUE_DETECT_3D,
     @SerializedName("unit_care_lifespan")
     UNIT_CARE_LIFESPAN,
+    @SerializedName("uses_clean_results_log_api")
+    USES_CLEAN_RESULTS_LOG_API,
     // implicit capabilities added in code
     EDGE_CLEANING,
     SPOT_CLEANING,
diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/HashUtil.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/HashUtil.java
new file mode 100644 (file)
index 0000000..1fb4252
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * 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.ecovacs.internal.api.util;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Johannes Ptaszyk - Initial contribution
+ */
+@NonNullByDefault
+public class HashUtil {
+    private static final Logger LOGGER = LoggerFactory.getLogger(HashUtil.class);
+
+    private HashUtil() {
+        // Prevent instantiation of util class
+    }
+
+    public static String getMD5Hash(String input) {
+        return calculateHash("MD5", input);
+    }
+
+    public static String getSHA256Hash(String input) {
+        return calculateHash("SHA-256", input);
+    }
+
+    private static String calculateHash(String algorithm, String input) {
+        MessageDigest md;
+        try {
+            md = MessageDigest.getInstance(algorithm);
+        } catch (NoSuchAlgorithmException e) {
+            LOGGER.error("Could not get {} MessageDigest instance", algorithm, e);
+            return "";
+        }
+        md.update(input.getBytes());
+        StringBuilder hexString = new StringBuilder();
+        for (byte b : md.digest()) {
+            if ((b & 0xff) < 0x10) {
+                hexString.append("0");
+            }
+            hexString.append(Integer.toHexString(b & 0xff));
+        }
+        return hexString.toString();
+    }
+}
diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/MD5Util.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/MD5Util.java
deleted file mode 100644 (file)
index 8ed8316..0000000
+++ /dev/null
@@ -1,53 +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.ecovacs.internal.api.util;
-
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * @author Johannes Ptaszyk - Initial contribution
- */
-@NonNullByDefault
-public class MD5Util {
-    private static final Logger LOGGER = LoggerFactory.getLogger(MD5Util.class);
-
-    private MD5Util() {
-        // Prevent instantiation of util class
-    }
-
-    public static String getMD5Hash(String input) {
-        MessageDigest md;
-        try {
-            md = MessageDigest.getInstance("MD5");
-        } catch (NoSuchAlgorithmException e) {
-            LOGGER.error("Could not get MD5 MessageDigest instance", e);
-            return "";
-        }
-        md.update(input.getBytes());
-        byte[] hash = md.digest();
-        StringBuilder hexString = new StringBuilder();
-        for (byte b : hash) {
-            if ((0xff & b) < 0x10) {
-                hexString.append("0").append(Integer.toHexString((0xFF & b)));
-            } else {
-                hexString.append(Integer.toHexString(0xFF & b));
-            }
-        }
-        return hexString.toString();
-    }
-}
index 7b4c2974db34da22b12aa99afa50ad7b5f9d6f4a..69680572a340ab7ec98fc858b94e2bd657cd8cbc 100644 (file)
@@ -122,7 +122,7 @@ public class EcovacsApiHandler extends BaseBridgeHandler {
         String deviceId = config.installId + deviceIdSuffix;
         org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration apiConfig = new org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration(
                 deviceId, config.email, config.password, config.continent, country, "EN", CLIENT_KEY, CLIENT_SECRET,
-                AUTH_CLIENT_KEY, AUTH_CLIENT_SECRET);
+                AUTH_CLIENT_KEY, AUTH_CLIENT_SECRET, APP_KEY);
 
         return EcovacsApi.create(httpClient, apiConfig);
     }
index 32a5881ae2c819b74f7bb58673e242a67faf127b..0a24b2eaa58a56d79cca24a96c85532f70a27e4f 100644 (file)
@@ -80,7 +80,6 @@ import org.openhab.binding.ecovacs.internal.util.StateOptionMapping;
 import org.openhab.core.i18n.ConfigurationException;
 import org.openhab.core.i18n.LocaleProvider;
 import org.openhab.core.i18n.TranslationProvider;
-import org.openhab.core.io.net.http.HttpUtil;
 import org.openhab.core.library.types.DateTimeType;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.OnOffType;
@@ -594,19 +593,11 @@ public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDev
 
                     if (device.hasCapability(DeviceCapability.MAPPING)
                             && !lastDownloadedCleanMapUrl.equals(record.mapImageUrl)) {
-                        updateState(CHANNEL_ID_LAST_CLEAN_MAP, record.mapImageUrl.flatMap(url -> {
-                            // HttpUtil expects the server to return the correct MIME type, but Ecovacs' server sends
-                            // 'application/octet-stream', so we have to set the correct MIME type by ourselves
-                            @Nullable
-                            RawType mapData = HttpUtil.downloadData(url, null, false, -1);
-                            if (mapData != null) {
-                                mapData = new RawType(mapData.getBytes(), "image/png");
-                                lastDownloadedCleanMapUrl = record.mapImageUrl;
-                            } else {
-                                logger.debug("{}: Downloading cleaning map {} failed", serialNumber, url);
-                            }
-                            return Optional.ofNullable((State) mapData);
-                        }).orElse(UnDefType.NULL));
+                        Optional<State> content = device.downloadCleanMapImage(record).map(bytes -> {
+                            lastDownloadedCleanMapUrl = record.mapImageUrl;
+                            return new RawType(bytes, "image/png");
+                        });
+                        updateState(CHANNEL_ID_LAST_CLEAN_MAP, content.orElse(UnDefType.NULL));
                     }
                 }
             }
index c585cf4baf1ef496da2f7146c7368cea018bbc4d..fbd19dfc535febbdb0a35a7fce6278b3f5a8053f 100644 (file)
         "deviceClass": "vdehg6",
         "deviceClassLink": "fqxoiu"
     },
+    {
+        "modelName": "DEEBOT N8 PRO+",
+        "deviceClass": "85as7h",
+        "deviceClassLink": "fqxoiu"
+    },
+    {
+        "modelName": "DEEBOT N8 PRO+",
+        "deviceClass": "ifbw08",
+        "deviceClassLink": "fqxoiu"
+    },
+
     {
         "modelName": "DEEBOT T9+",
         "deviceClass": "lhbd50",
-        "deviceClassLink": "fqxoiu"
+        "protoVersion": "json_v2",
+        "usesMqtt": true,
+        "capabilities": [
+            "mopping_system",
+            "main_brush",
+            "spot_area_cleaning",
+            "custom_area_cleaning",
+            "clean_speed_control",
+            "voice_reporting",
+            "read_network_info",
+            "unit_care_lifespan",
+            "true_detect_3d",
+            "mapping",
+            "auto_empty_station",
+            "uses_clean_results_log_api"
+        ]
     },
     {
         "modelName": "DEEBOT T9+",
         "deviceClass": "um2ywg",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT T9 AIVI",
         "deviceClass": "8kwdb4",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT T9 AIVI",
         "deviceClass": "659yh8",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT T9 AIVI Plus",
         "deviceClass": "kw9ayx",
-        "deviceClassLink": "fqxoiu"
-    },
-    {
-        "modelName": "DEEBOT N8 PRO+",
-        "deviceClass": "85as7h",
-        "deviceClassLink": "fqxoiu"
-    },
-    {
-        "modelName": "DEEBOT N8 PRO+",
-        "deviceClass": "ifbw08",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT N9+",
         "deviceClass": "a7lhb1",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT N9+",
         "deviceClass": "c2of2s",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT X1",
         "deviceClass": "3yqsch",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT T10",
         "deviceClass": "jtmf04",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT T10 PLUS",
         "deviceClass": "rss8xk",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT T10 PLUS",
         "deviceClass": "p95mgv",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT T10 TURBO",
         "deviceClass": "9s1s80",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT T10 OMNI",
         "deviceClass": "lx3j7m",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT X1 OMNI",
         "deviceClass": "8bja83",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT X1 OMNI",
         "deviceClass": "1b23du",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT X1 OMNI",
         "deviceClass": "1vxt52",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT X1 TURBO",
         "deviceClass": "2o4lnm",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT X1 PLUS",
         "deviceClass": "n4gstt",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT X1e OMNI",
         "deviceClass": "bro5wu",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT T20 OMNI",
         "deviceClass": "p1jij8",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT N10 PLUS",
         "deviceClass": "umwv6z",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
     {
         "modelName": "DEEBOT N10 MAX+",
         "deviceClass": "clojes",
-        "deviceClassLink": "fqxoiu"
+        "deviceClassLink": "lhbd50"
     },
 
     {
             "unit_care_lifespan",
             "true_detect_3d",
             "mapping",
-            "auto_empty_station"
+            "auto_empty_station",
+            "uses_clean_results_log_api"
         ]
     },
     {