]> git.basschouten.com Git - openhab-addons.git/commitdiff
[ecovacs] Allow loading device descriptions from a user file (#14873)
authormaniac103 <dannybaumann@web.de>
Mon, 1 May 2023 13:44:56 +0000 (15:44 +0200)
committerGitHub <noreply@github.com>
Mon, 1 May 2023 13:44:56 +0000 (15:44 +0200)
* [ecovacs] Allow loading device descriptions from a user file

If a new device is released within our release cycle, getting the
description for that device into the binding was impossible for users
before. With this change, they can create an override file in their
installation (with contents we'll need to supply to them), which serves
as a stop-gap solution until the next OH release.

Signed-off-by: Danny Baumann <dannybaumann@web.de>
bundles/org.openhab.binding.ecovacs/README.md
bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiImpl.java
bundles/org.openhab.binding.ecovacs/src/main/resources/devices/supported_device_list.json

index 98284ba23de56c553ad671c57c4d271200568667..3360cf0ee965cf184389f004f04ed3cdede3648b 100644 (file)
@@ -173,3 +173,20 @@ Bridge ecovacs:ecovacsapi:ecovacsapi [ email="your.email@provider.com", password
 }
 ```
 
+## Adding support for unsupported models
+
+When encountering an unsupported model during discovery, the binding creates a log message like this one:
+
+```
+2023-04-21 12:02:39.607 [INFO ] [acs.internal.api.impl.EcovacsApiImpl] - Found unsupported device DEEBOT N8 PRO CARE (class s1f8g7, company eco-ng), ignoring.
+```
+
+In such a case, please [create an issue on GitHub](https://github.com/openhab/openhab-addons/issues), listing the contents of the log line.
+In addition to that, if the model is similar to an already supported one, you can try to add the support yourself (until getting an updated binding).
+For doing so, you can follow the following steps:
+
+- create the folder `<OPENHAB_USERDATA>/evocacs` (if not done previously)
+- create a file named `custom_device_descs.json`, whose format of that file is the same as [the built-in device list](https://raw.githubusercontent.com/openhab/openhab-addons/main/bundles/org.openhab.binding.ecovacs/src/main/resources/devices/supported_device_list.json)
+- for a model that is very similar to an existing one, create an entry with `modelName`, `deviceClass` (from the log line) and `deviceClassLink` (`deviceClass` of the similar model)
+- for other models, you can also try experimenting with creating a full entry, but it's likely that the binding code will need to be updated in that case
+
index 2cf1f784b9833863b71b1390fde1e04e198edf58..2411fe5041ff2a69902562a9f0ca2cf087e3914e 100644 (file)
  */
 package org.openhab.binding.ecovacs.internal.api.impl;
 
-import java.io.InputStream;
+import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.Reader;
 import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -25,7 +29,6 @@ import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import java.util.stream.Collectors;
 
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.transform.TransformerException;
@@ -64,10 +67,12 @@ import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalI
 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.core.OpenHAB;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonReader;
 
@@ -162,12 +167,11 @@ public final class EcovacsApiImpl implements EcovacsApi {
 
     @Override
     public List<EcovacsDevice> getDevices() throws EcovacsApiException, InterruptedException {
-        List<DeviceDescription> descriptions = getSupportedDeviceList();
+        Map<String, DeviceDescription> descriptions = getSupportedDeviceDescs();
         List<IotProduct> products = null;
         List<EcovacsDevice> devices = new ArrayList<>();
         for (Device dev : getDeviceList()) {
-            Optional<DeviceDescription> descOpt = descriptions.stream()
-                    .filter(d -> dev.getDeviceClass().equals(d.deviceClass)).findFirst();
+            Optional<DeviceDescription> descOpt = Optional.ofNullable(descriptions.get(dev.getDeviceClass()));
             if (!descOpt.isPresent()) {
                 if (products == null) {
                     products = getIotProductMap();
@@ -188,29 +192,58 @@ public final class EcovacsApiImpl implements EcovacsApi {
         return devices;
     }
 
-    private List<DeviceDescription> getSupportedDeviceList() {
+    // maps device class -> device description
+    private Map<String, DeviceDescription> getSupportedDeviceDescs() {
+        Map<String, DeviceDescription> descs = new HashMap<>();
         ClassLoader cl = Objects.requireNonNull(getClass().getClassLoader());
-        InputStream is = cl.getResourceAsStream("devices/supported_device_list.json");
-        JsonReader reader = new JsonReader(new InputStreamReader(is));
-        Type type = new TypeToken<List<DeviceDescription>>() {
-        }.getType();
-        List<DeviceDescription> descs = gson.fromJson(reader, type);
-        return descs.stream().map(desc -> {
-            final DeviceDescription result;
+        try (Reader reader = new InputStreamReader(cl.getResourceAsStream("devices/supported_device_list.json"))) {
+            for (DeviceDescription desc : loadSupportedDeviceData(reader)) {
+                descs.put(desc.deviceClass, desc);
+            }
+            logger.trace("Loaded {} built-in device descriptions", descs.size());
+        } catch (IOException | JsonSyntaxException e) {
+            logger.warn("Failed loading built-in device descriptions", e);
+        }
+
+        Path customDescsPath = Paths.get(OpenHAB.getUserDataFolder(), "ecovacs").resolve("custom_device_descs.json");
+        if (Files.exists(customDescsPath)) {
+            try (Reader reader = Files.newBufferedReader(customDescsPath)) {
+                int builtins = descs.size();
+                for (DeviceDescription desc : loadSupportedDeviceData(reader)) {
+                    DeviceDescription builtinDesc = descs.put(desc.deviceClass, desc);
+                    if (builtinDesc != null) {
+                        logger.trace("Overriding built-in description for {} with custom description",
+                                desc.deviceClass);
+                    }
+                }
+                logger.trace("Loaded {} custom device descriptions", descs.size() - builtins);
+            } catch (IOException | JsonSyntaxException e) {
+                logger.warn("Failed loading custom device descriptions from {}", customDescsPath, e);
+            }
+        }
+
+        descs.entrySet().forEach(descEntry -> {
+            DeviceDescription desc = descEntry.getValue();
             if (desc.deviceClassLink != null) {
-                Optional<DeviceDescription> linkedDescOpt = descs.stream()
-                        .filter(d -> d.deviceClass.equals(desc.deviceClassLink)).findFirst();
+                Optional<DeviceDescription> linkedDescOpt = Optional.ofNullable(descs.get(desc.deviceClassLink));
                 if (!linkedDescOpt.isPresent()) {
-                    throw new IllegalStateException(
-                            "Desc " + desc.deviceClass + " links unknown desc " + desc.deviceClassLink);
+                    logger.warn("Device description {} links unknown description {}", desc.deviceClass,
+                            desc.deviceClassLink);
                 }
-                result = desc.resolveLinkWith(linkedDescOpt.get());
-            } else {
-                result = desc;
+                desc = desc.resolveLinkWith(linkedDescOpt.get());
+                descEntry.setValue(desc);
             }
-            result.addImplicitCapabilities();
-            return result;
-        }).collect(Collectors.toList());
+            desc.addImplicitCapabilities();
+        });
+
+        return descs;
+    }
+
+    private List<DeviceDescription> loadSupportedDeviceData(Reader input) throws IOException {
+        JsonReader reader = new JsonReader(input);
+        Type type = new TypeToken<List<DeviceDescription>>() {
+        }.getType();
+        return gson.fromJson(reader, type);
     }
 
     private List<Device> getDeviceList() throws EcovacsApiException, InterruptedException {
index 36985c9325559de857b1a9cdad2e8d547ba2c517..30c1c815f4ccaa53dbcd418098370f1b0b63adf5 100644 (file)
         "deviceClass": "yu362x",
         "deviceClassLink": "h18jkh"
     },
+    {
+        "modelName": "DEEBOT N8 PRO CARE",
+        "deviceClass": "s1f8g7",
+        "deviceClassLink": "h18jkh"
+    },
 
     {
         "modelName": "DEEBOT OZMO T8+",