Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ecovacs] Allow loading device descriptions from a user file #14873

Merged
merged 5 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions bundles/org.openhab.binding.ecovacs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,20 @@ Bridge ecovacs:ecovacsapi:ecovacsapi [ email="[email protected]", 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

Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
*/
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;
Expand All @@ -25,7 +29,6 @@
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;
Expand Down Expand Up @@ -64,10 +67,12 @@
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;

Expand Down Expand Up @@ -162,12 +167,11 @@ private PortalLoginResponse portalLogin(AuthCode authCode, AccessData accessData

@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();
Expand All @@ -188,29 +192,58 @@ public List<EcovacsDevice> getDevices() throws EcovacsApiException, InterruptedE
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@
"deviceClass": "yu362x",
"deviceClassLink": "h18jkh"
},
{
"modelName": "DEEBOT N8 PRO CARE",
"deviceClass": "s1f8g7",
"deviceClassLink": "h18jkh"
},

{
"modelName": "DEEBOT OZMO T8+",
Expand Down