[ecovacs] Add support for new API for fetching cleaning logs (#16524)

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>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
maniac103 2024-03-17 20:47:36 +01:00 committed by Ciprian Pascu
parent 41b9a02dea
commit 13940df9bd
15 changed files with 289 additions and 119 deletions

View File

@ -37,6 +37,7 @@ public class EcovacsBindingConstants {
public static final String CLIENT_SECRET = "6c319b2a5cd3e66e39159c2e28f2fce9"; public static final String CLIENT_SECRET = "6c319b2a5cd3e66e39159c2e28f2fce9";
public static final String AUTH_CLIENT_KEY = "1520391491841"; public static final String AUTH_CLIENT_KEY = "1520391491841";
public static final String AUTH_CLIENT_SECRET = "77ef58ce3afbe337da74aa8c5ab963a9"; public static final String AUTH_CLIENT_SECRET = "77ef58ce3afbe337da74aa8c5ab963a9";
public static final String APP_KEY = "2ea31cf06e6711eaa0aff7b9558a534e";
// List of all Thing Type UIDs // List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_API = new ThingTypeUID(BINDING_ID, "ecovacsapi"); public static final ThingTypeUID THING_TYPE_API = new ThingTypeUID(BINDING_ID, "ecovacsapi");

View File

@ -13,7 +13,7 @@
package org.openhab.binding.ecovacs.internal.api; package org.openhab.binding.ecovacs.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault; 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 * @author Johannes Ptaszyk - Initial contribution
@ -30,10 +30,12 @@ public final class EcovacsApiConfiguration {
private final String clientSecret; private final String clientSecret;
private final String authClientKey; private final String authClientKey;
private final String authClientSecret; private final String authClientSecret;
private final String appKey;
public EcovacsApiConfiguration(String deviceId, String username, String password, String continent, String country, public EcovacsApiConfiguration(String deviceId, String username, String password, String continent, String country,
String language, String clientKey, String clientSecret, String authClientKey, String authClientSecret) { String language, String clientKey, String clientSecret, String authClientKey, String authClientSecret,
this.deviceId = MD5Util.getMD5Hash(deviceId); String appKey) {
this.deviceId = HashUtil.getMD5Hash(deviceId);
this.username = username; this.username = username;
this.password = password; this.password = password;
this.continent = continent; this.continent = continent;
@ -43,6 +45,7 @@ public final class EcovacsApiConfiguration {
this.clientSecret = clientSecret; this.clientSecret = clientSecret;
this.authClientKey = authClientKey; this.authClientKey = authClientKey;
this.authClientSecret = authClientSecret; this.authClientSecret = authClientSecret;
this.appKey = appKey;
} }
public String getDeviceId() { public String getDeviceId() {
@ -90,7 +93,7 @@ public final class EcovacsApiConfiguration {
return "ecouser.net"; return "ecouser.net";
} }
public String getPortalAUthRequestWith() { public String getPortalAuthRequestWith() {
return "users"; return "users";
} }
@ -110,12 +113,28 @@ public final class EcovacsApiConfiguration {
return "google_play"; return "google_play";
} }
public String getAppId() {
return "ecovacs";
}
public String getAppPlatform() {
return "android";
}
public String getAppCode() { public String getAppCode() {
return "global_e"; return "global_e";
} }
public String getAppVersion() { 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() { public String getDeviceType() {

View File

@ -59,4 +59,6 @@ public interface EcovacsDevice {
<T> T sendCommand(IotDeviceCommand<T> command) throws EcovacsApiException, InterruptedException; <T> T sendCommand(IotDeviceCommand<T> command) throws EcovacsApiException, InterruptedException;
List<CleanLogRecord> getCleanLogs() throws EcovacsApiException, InterruptedException; List<CleanLogRecord> getCleanLogs() throws EcovacsApiException, InterruptedException;
Optional<byte[]> downloadCleanMapImage(CleanLogRecord record) throws EcovacsApiException, InterruptedException;
} }

View 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.AbstractPortalIotCommandResponse;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.Device; 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.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.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.PortalDeviceResponse;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandJsonResponse; 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.PortalIotCommandXmlResponse;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotProductResponse; 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.impl.dto.response.portal.PortalLoginResponse;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException; 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.openhab.core.OpenHAB;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -116,8 +118,8 @@ public final class EcovacsApiImpl implements EcovacsApi {
private AccessData login() throws EcovacsApiException, InterruptedException { private AccessData login() throws EcovacsApiException, InterruptedException {
HashMap<String, String> loginParameters = new HashMap<>(); HashMap<String, String> loginParameters = new HashMap<>();
loginParameters.put("account", configuration.getUsername()); loginParameters.put("account", configuration.getUsername());
loginParameters.put("password", MD5Util.getMD5Hash(configuration.getPassword())); loginParameters.put("password", HashUtil.getMD5Hash(configuration.getPassword()));
loginParameters.put("requestId", MD5Util.getMD5Hash(String.valueOf(System.currentTimeMillis()))); loginParameters.put("requestId", HashUtil.getMD5Hash(String.valueOf(System.currentTimeMillis())));
loginParameters.put("authTimeZone", configuration.getTimeZone()); loginParameters.put("authTimeZone", configuration.getTimeZone());
loginParameters.put("country", configuration.getCountry()); loginParameters.put("country", configuration.getCountry());
loginParameters.put("lang", configuration.getLanguage()); loginParameters.put("lang", configuration.getLanguage());
@ -310,8 +312,7 @@ public final class EcovacsApiImpl implements EcovacsApi {
} }
} }
public List<PortalCleanLogsResponse.LogRecord> fetchCleanLogs(Device device) public List<PortalCleanLogRecord> fetchCleanLogs(Device device) throws EcovacsApiException, InterruptedException {
throws EcovacsApiException, InterruptedException {
PortalCleanLogsRequest data = new PortalCleanLogsRequest(createAuthData(), device.getDid(), PortalCleanLogsRequest data = new PortalCleanLogsRequest(createAuthData(), device.getDid(),
device.getResource()); device.getResource());
String url = EcovacsApiUrlFactory.getPortalLogUrl(configuration); String url = EcovacsApiUrlFactory.getPortalLogUrl(configuration);
@ -324,12 +325,39 @@ public final class EcovacsApiImpl implements EcovacsApi {
return responseObj.records; 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() { private PortalAuthRequestParameter createAuthData() {
PortalLoginResponse loginData = this.loginData; PortalLoginResponse loginData = this.loginData;
if (loginData == null) { if (loginData == null) {
throw new IllegalStateException("Not logged in"); 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()); configuration.getRealm(), loginData.getToken(), configuration.getResource());
} }
@ -371,7 +399,7 @@ public final class EcovacsApiImpl implements EcovacsApi {
signOnText.append(clientSecret); signOnText.append(clientSecret);
signedRequestParameters.put("authAppkey", clientKey); 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); Request request = httpClient.newRequest(url).method(HttpMethod.GET);
signedRequestParameters.forEach(request::param); signedRequestParameters.forEach(request::param);
@ -379,6 +407,27 @@ public final class EcovacsApiImpl implements EcovacsApi {
return request; 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) { private Request createJsonRequest(String url, Object data) {
return httpClient.newRequest(url).method(HttpMethod.POST).header(HttpHeader.CONTENT_TYPE, "application/json") return httpClient.newRequest(url).method(HttpMethod.POST).header(HttpHeader.CONTENT_TYPE, "application/json")
.content(new StringContentProvider(gson.toJson(data))); .content(new StringContentProvider(gson.toJson(data)));

View File

@ -27,10 +27,11 @@ public final class EcovacsApiUrlFactory {
private static final String MAIN_URL_LOGIN_PATH = "/user/login"; 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_USERS_PATH = "/api/users/user.do";
private static final String PORTAL_IOT_PRODUCT_PATH = "/pim/product/getProductIotMap"; private static final String PORTAL_IOT_PRODUCT_PATH = "/api/pim/product/getProductIotMap";
private static final String PORTAL_IOT_DEVMANAGER_PATH = "/iot/devmanager.do"; private static final String PORTAL_IOT_DEVMANAGER_PATH = "/api/iot/devmanager.do";
private static final String PORTAL_LOG_PATH = "/lg/log.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) { public static String getLoginUrl(EcovacsApiConfiguration config) {
return getMainUrl(config) + MAIN_URL_LOGIN_PATH; return getMainUrl(config) + MAIN_URL_LOGIN_PATH;
@ -57,9 +58,13 @@ public final class EcovacsApiUrlFactory {
return getPortalUrl(config) + PORTAL_LOG_PATH; 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) { private static String getPortalUrl(EcovacsApiConfiguration config) {
String continentSuffix = "cn".equalsIgnoreCase(config.getCountry()) ? "" : "-" + config.getContinent(); 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) { private static String getMainUrl(EcovacsApiConfiguration config) {

View 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.GetFirmwareVersionCommand;
import org.openhab.binding.ecovacs.internal.api.commands.IotDeviceCommand; 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.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.impl.dto.response.portal.PortalLoginResponse;
import org.openhab.binding.ecovacs.internal.api.model.CleanLogRecord; import org.openhab.binding.ecovacs.internal.api.model.CleanLogRecord;
import org.openhab.binding.ecovacs.internal.api.model.DeviceCapability; import org.openhab.binding.ecovacs.internal.api.model.DeviceCapability;
@ -103,12 +104,25 @@ public class EcovacsIotMqDevice implements EcovacsDevice {
if (desc.protoVersion == ProtocolVersion.XML) { if (desc.protoVersion == ProtocolVersion.XML) {
logEntries = sendCommand(new GetCleanLogsCommand()).stream(); logEntries = sendCommand(new GetCleanLogsCommand()).stream();
} else { } else {
logEntries = api.fetchCleanLogs(device).stream().map(record -> new CleanLogRecord(record.timestamp, List<PortalCleanLogRecord> log = hasCapability(DeviceCapability.USES_CLEAN_RESULTS_LOG_API)
record.duration, record.area, Optional.ofNullable(record.imageUrl), record.type)); ? 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()); 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 @Override
public void connect(final EventListener listener, ScheduledExecutorService scheduler) public void connect(final EventListener listener, ScheduledExecutorService scheduler)
throws EcovacsApiException, InterruptedException { throws EcovacsApiException, InterruptedException {

View File

@ -153,6 +153,12 @@ public class EcovacsXmppDevice implements EcovacsDevice {
return sendCommand(new GetCleanLogsCommand()); return sendCommand(new GetCleanLogsCommand());
} }
@Override
public Optional<byte[]> downloadCleanMapImage(CleanLogRecord record)
throws EcovacsApiException, InterruptedException {
return Optional.empty();
}
@Override @Override
public void connect(final EventListener listener, final ScheduledExecutorService scheduler) public void connect(final EventListener listener, final ScheduledExecutorService scheduler)
throws EcovacsApiException { throws EcovacsApiException {

View File

@ -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;
}
}

View File

@ -14,48 +14,19 @@ package org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal;
import java.util.List; import java.util.List;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
/** /**
* @author Johannes Ptaszyk - Initial contribution * @author Johannes Ptaszyk - Initial contribution
*/ */
public class PortalCleanLogsResponse { 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") @SerializedName("logs")
public final List<LogRecord> records; public final List<PortalCleanLogRecord> records;
@SerializedName("ret") @SerializedName("ret")
final String result; final String result;
PortalCleanLogsResponse(String result, List<LogRecord> records) { PortalCleanLogsResponse(String result, List<PortalCleanLogRecord> records) {
this.result = result; this.result = result;
this.records = records; this.records = records;
} }

View File

@ -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;
}
}

View File

@ -47,6 +47,8 @@ public enum DeviceCapability {
TRUE_DETECT_3D, TRUE_DETECT_3D,
@SerializedName("unit_care_lifespan") @SerializedName("unit_care_lifespan")
UNIT_CARE_LIFESPAN, UNIT_CARE_LIFESPAN,
@SerializedName("uses_clean_results_log_api")
USES_CLEAN_RESULTS_LOG_API,
// implicit capabilities added in code // implicit capabilities added in code
EDGE_CLEANING, EDGE_CLEANING,
SPOT_CLEANING, SPOT_CLEANING,

View File

@ -23,30 +23,36 @@ import org.slf4j.LoggerFactory;
* @author Johannes Ptaszyk - Initial contribution * @author Johannes Ptaszyk - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class MD5Util { public class HashUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(MD5Util.class); private static final Logger LOGGER = LoggerFactory.getLogger(HashUtil.class);
private MD5Util() { private HashUtil() {
// Prevent instantiation of util class // Prevent instantiation of util class
} }
public static String getMD5Hash(String input) { 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; MessageDigest md;
try { try {
md = MessageDigest.getInstance("MD5"); md = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
LOGGER.error("Could not get MD5 MessageDigest instance", e); LOGGER.error("Could not get {} MessageDigest instance", algorithm, e);
return ""; return "";
} }
md.update(input.getBytes()); md.update(input.getBytes());
byte[] hash = md.digest();
StringBuilder hexString = new StringBuilder(); StringBuilder hexString = new StringBuilder();
for (byte b : hash) { for (byte b : md.digest()) {
if ((0xff & b) < 0x10) { if ((b & 0xff) < 0x10) {
hexString.append("0").append(Integer.toHexString((0xFF & b))); hexString.append("0");
} else {
hexString.append(Integer.toHexString(0xFF & b));
} }
hexString.append(Integer.toHexString(b & 0xff));
} }
return hexString.toString(); return hexString.toString();
} }

View File

@ -122,7 +122,7 @@ public class EcovacsApiHandler extends BaseBridgeHandler {
String deviceId = config.installId + deviceIdSuffix; String deviceId = config.installId + deviceIdSuffix;
org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration apiConfig = new org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration( 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, 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); return EcovacsApi.create(httpClient, apiConfig);
} }

View File

@ -80,7 +80,6 @@ import org.openhab.binding.ecovacs.internal.util.StateOptionMapping;
import org.openhab.core.i18n.ConfigurationException; import org.openhab.core.i18n.ConfigurationException;
import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider; 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.DateTimeType;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
@ -594,19 +593,11 @@ public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDev
if (device.hasCapability(DeviceCapability.MAPPING) if (device.hasCapability(DeviceCapability.MAPPING)
&& !lastDownloadedCleanMapUrl.equals(record.mapImageUrl)) { && !lastDownloadedCleanMapUrl.equals(record.mapImageUrl)) {
updateState(CHANNEL_ID_LAST_CLEAN_MAP, record.mapImageUrl.flatMap(url -> { Optional<State> content = device.downloadCleanMapImage(record).map(bytes -> {
// HttpUtil expects the server to return the correct MIME type, but Ecovacs' server sends lastDownloadedCleanMapUrl = record.mapImageUrl;
// 'application/octet-stream', so we have to set the correct MIME type by ourselves return new RawType(bytes, "image/png");
@Nullable });
RawType mapData = HttpUtil.downloadData(url, null, false, -1); updateState(CHANNEL_ID_LAST_CLEAN_MAP, content.orElse(UnDefType.NULL));
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));
} }
} }
} }

View File

@ -354,31 +354,6 @@
"deviceClass": "vdehg6", "deviceClass": "vdehg6",
"deviceClassLink": "fqxoiu" "deviceClassLink": "fqxoiu"
}, },
{
"modelName": "DEEBOT T9+",
"deviceClass": "lhbd50",
"deviceClassLink": "fqxoiu"
},
{
"modelName": "DEEBOT T9+",
"deviceClass": "um2ywg",
"deviceClassLink": "fqxoiu"
},
{
"modelName": "DEEBOT T9 AIVI",
"deviceClass": "8kwdb4",
"deviceClassLink": "fqxoiu"
},
{
"modelName": "DEEBOT T9 AIVI",
"deviceClass": "659yh8",
"deviceClassLink": "fqxoiu"
},
{
"modelName": "DEEBOT T9 AIVI Plus",
"deviceClass": "kw9ayx",
"deviceClassLink": "fqxoiu"
},
{ {
"modelName": "DEEBOT N8 PRO+", "modelName": "DEEBOT N8 PRO+",
"deviceClass": "85as7h", "deviceClass": "85as7h",
@ -389,90 +364,131 @@
"deviceClass": "ifbw08", "deviceClass": "ifbw08",
"deviceClassLink": "fqxoiu" "deviceClassLink": "fqxoiu"
}, },
{
"modelName": "DEEBOT T9+",
"deviceClass": "lhbd50",
"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": "lhbd50"
},
{
"modelName": "DEEBOT T9 AIVI",
"deviceClass": "8kwdb4",
"deviceClassLink": "lhbd50"
},
{
"modelName": "DEEBOT T9 AIVI",
"deviceClass": "659yh8",
"deviceClassLink": "lhbd50"
},
{
"modelName": "DEEBOT T9 AIVI Plus",
"deviceClass": "kw9ayx",
"deviceClassLink": "lhbd50"
},
{ {
"modelName": "DEEBOT N9+", "modelName": "DEEBOT N9+",
"deviceClass": "a7lhb1", "deviceClass": "a7lhb1",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT N9+", "modelName": "DEEBOT N9+",
"deviceClass": "c2of2s", "deviceClass": "c2of2s",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT X1", "modelName": "DEEBOT X1",
"deviceClass": "3yqsch", "deviceClass": "3yqsch",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT T10", "modelName": "DEEBOT T10",
"deviceClass": "jtmf04", "deviceClass": "jtmf04",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT T10 PLUS", "modelName": "DEEBOT T10 PLUS",
"deviceClass": "rss8xk", "deviceClass": "rss8xk",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT T10 PLUS", "modelName": "DEEBOT T10 PLUS",
"deviceClass": "p95mgv", "deviceClass": "p95mgv",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT T10 TURBO", "modelName": "DEEBOT T10 TURBO",
"deviceClass": "9s1s80", "deviceClass": "9s1s80",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT T10 OMNI", "modelName": "DEEBOT T10 OMNI",
"deviceClass": "lx3j7m", "deviceClass": "lx3j7m",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT X1 OMNI", "modelName": "DEEBOT X1 OMNI",
"deviceClass": "8bja83", "deviceClass": "8bja83",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT X1 OMNI", "modelName": "DEEBOT X1 OMNI",
"deviceClass": "1b23du", "deviceClass": "1b23du",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT X1 OMNI", "modelName": "DEEBOT X1 OMNI",
"deviceClass": "1vxt52", "deviceClass": "1vxt52",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT X1 TURBO", "modelName": "DEEBOT X1 TURBO",
"deviceClass": "2o4lnm", "deviceClass": "2o4lnm",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT X1 PLUS", "modelName": "DEEBOT X1 PLUS",
"deviceClass": "n4gstt", "deviceClass": "n4gstt",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT X1e OMNI", "modelName": "DEEBOT X1e OMNI",
"deviceClass": "bro5wu", "deviceClass": "bro5wu",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT T20 OMNI", "modelName": "DEEBOT T20 OMNI",
"deviceClass": "p1jij8", "deviceClass": "p1jij8",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT N10 PLUS", "modelName": "DEEBOT N10 PLUS",
"deviceClass": "umwv6z", "deviceClass": "umwv6z",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
"modelName": "DEEBOT N10 MAX+", "modelName": "DEEBOT N10 MAX+",
"deviceClass": "clojes", "deviceClass": "clojes",
"deviceClassLink": "fqxoiu" "deviceClassLink": "lhbd50"
}, },
{ {
@ -576,7 +592,8 @@
"unit_care_lifespan", "unit_care_lifespan",
"true_detect_3d", "true_detect_3d",
"mapping", "mapping",
"auto_empty_station" "auto_empty_station",
"uses_clean_results_log_api"
] ]
}, },
{ {