[ecovacs] Initial contribution (#12231)

* [ecovacs] Initial contribution

Add initial version of a binding for vacuum cleaners made by Ecovacs.

Signed-off-by: Danny Baumann <dannybaumann@web.de>
This commit is contained in:
maniac103 2023-03-21 11:05:53 +01:00 committed by GitHub
parent 98b8d7225c
commit b47a205f44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
128 changed files with 9421 additions and 0 deletions

View File

@ -82,6 +82,7 @@
/bundles/org.openhab.binding.echonetlite/ @mikeb01
/bundles/org.openhab.binding.ecobee/ @mhilbush
/bundles/org.openhab.binding.ecotouch/ @sibbi77
/bundles/org.openhab.binding.ecovacs/ @maniac103
/bundles/org.openhab.binding.ecowatt/ @lolodomo
/bundles/org.openhab.binding.ekey/ @hmerk
/bundles/org.openhab.binding.electroluxair/ @jannegpriv

View File

@ -406,6 +406,11 @@
<artifactId>org.openhab.binding.ecotouch</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.ecovacs</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.ecowatt</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,175 @@
# Ecovacs Binding
This binding provides integration for vacuum cleaning / mopping robots made by Ecovacs (<https://www.ecovacs.com/>).
It discovers devices and communicates to them by using Ecovacs' cloud services.
## Supported Things
- Ecovacs cloud API (`ecovacsapi`)
- Vacuum cleaner (`vacuum`)
At this point, the following devices are fully supported and verified to be working:
- Deebot OZMO 900/905
- Deebot OZMO 920
- Deebot OZMO 930
- Deebot OZMO 950
- Deebot OZMO Slim 10/11
- Deebot N8 series
The following devices will likely work because they are using similar protocols as the above ones:
- Deebot 600/601/605
- Deebot 900/901
- Deebot OZMO 610
- Deebot 710/711/711s
- Deebot OZMO T5
- Deebot (OZMO) T8 series
- Deebot T9 series
- Deebot Slim 2
- Deebot N3 MAX
- Deebot N7
- Deebot U2 series
- Deebot X1 Omni
## Discovery
At first, you need to manually create the bridge thing for the cloud API.
Once that is done, the supported devices will be automatically discovered and added to the inbox.
## Thing Configuration
For the cloud API thing, the following parameters must be configured:
| Config | Description |
|-----------|-------------------------------------------------------------------------------------------------------------------------------|
| email | The email address you used when registering the Ecovacs cloud account |
| password | The cloud account password |
| continent | The continent you are residing on, or 'World' if none matches. This is used to select the correct cloud server to connect to. |
For the vacuum things, there is no required configuration (when using discovery). The following parameters exist:
| Config | Description |
|--------------|-------------------------------------------------------------------------------------------------------------------------------|
| serialNumber | Required: The device's serial number as printed on the barcode below the dust bin. Filled automatically when using discovery. |
| refresh | Refresh interval for polled data (see below), in minutes. By default set to 5 minutes. |
## Channels
The list below lists all channels supported by the binding.
In case a particular channel is not supported by a given device (see remarks), it is automatically removed from the given thing.
| Channel | Type | Description | Read Only | Updated By | Remarks |
|-----------------------------------------|----------------------|-----------------------------------------------------------|-----------|------------|----------|
| actions#command | String | Command to execute | No | Event | [1] |
| status#state | String | Current operational state | Yes | Event | [2] |
| status#current-cleaning-mode | String | Mode used in current cleaning run | Yes | Event | [3], [4] |
| status#current-cleaning-time | Number:Time | Time spent in current cleaning run | Yes | Event | [4] |
| status#current-cleaned-area | Number:Area | Area cleaned in current cleaning run | Yes | Event | [4] |
| status#current-cleaning-spot-definition | String | The spot to clean in current cleaning run | Yes | Event | [4], [5] |
| status#water-system-present | Switch | Whether the device is currently ready for mopping | Yes | Event | [6] |
| status#wifi-rssi | Number:Power | The current Wi-Fi signal strength of the device | Yes | Polling | [7] |
| consumables#main-brush-lifetime | Number:Dimensionless | The remaining life time of the main brush in percent | Yes | Polling | [8] |
| consumables#side-brush-lifetime | Number:Dimensionless | The remaining life time of the side brush in percent | Yes | Polling | |
| consumables#dust-filter-lifetime | Number:Dimensionless | The remaining life time of the dust bin filter in percent | Yes | Polling | |
| consumables#other-component-lifetime | Number:Dimensionless | The remaining time until device maintenance in percent | Yes | Polling | [9] |
| last-clean#last-clean-start | DateTime | The start time of the last completed cleaning run | Yes | Polling | |
| last-clean#last-clean-duration | Number:Time | The duration of the last completed cleaning run | Yes | Polling | |
| last-clean#last-clean-area | Number:Area | The area cleaned in the last completed cleaning run | Yes | Polling | |
| last-clean#last-clean-mode | String | The mode used for the last completed cleaning run | Yes | Polling | [3] |
| last-clean#last-clean-map | Image | The map image of the last completed cleaning run | Yes | Polling | |
| total-stats#total-cleaning-time | Number:Time | The total time spent cleaning during the device life time | Yes | Polling | |
| total-stats#total-cleaned-area | Number:Area | The total area cleaned during the device life time | Yes | Polling | |
| total-stats#total-clean-runs | Number | The total number of clean runs in the device life time | Yes | Polling | |
| settings#auto-empty | Switch | Whether dust bin auto empty to station is enabled | No | Polling | [10] |
| settings#cleaning-passes | Number | Number of cleaning passes to be used (1 or 2) | No | Polling | [9] |
| settings#continuous-cleaning | Switch | Whether unfinished cleaning resumes after charging | No | Polling | |
| settings#suction-power | String | The power level used during cleaning | No | Polling | [11] |
| settings#true-detect-3d | Switch | Whether True Detect 3D is enabled | No | Polling | [12] |
| settings#voice-volume | Dimmer | The voice volume level in percent | No | Polling | [13] |
| settings#water-amount | String | The amount of water to be used when mopping | No | Polling | [14] |
Remarks:
- [1] See [section below](#command-channel-actions)
- [2] Possible states: 'cleaning', 'pause', 'stop', 'drying', 'washing', 'returning' and 'charging' (where 'drying' and 'washing' are only available on newer models with auto empty station)
- [3] Possible states: 'auto', 'edge', 'spot', 'spotArea', 'customArea', 'singleRoom' (some of which depend on device capabilities)
- [4] Current cleaning status is only valid if the device is currently cleaning
- [5] Only valid for 'spot', 'spotArea' and 'customArea' cleaning modes; value can be used for 'spotArea' and 'customArea' commands (see below)
- [6] Only present if device has a mopping system
- [7] Only present on newer generation devices (Deebot OZMO 950 and newer)
- [8] Only present if device has a main brush
- [9] Only present on newer generation devices (Deebot N8/T8 or newer)
- [10] Only present if device has a dustbin auto empty station; supports both on/off command (to turn on/off the setting) and the string 'trigger' (to trigger immediate auto empty)
- [11] Only present if device can control power level. Possible values vary by device: 'normal' and 'high' are always supported, 'silent' and 'higher' are supported for some models
- [12] Only present if device supports True Detect 3D
- [13] Only present if device has voice reporting
- [14] Only present if device has a mopping system. Possible values include 'low', 'medium', 'high' and 'veryhigh'
## Command Channel Actions
The following actions are supported by the `command` channel:
| Name | Action | Remarks |
|--------------|-------------------------------------------|------------------------------------------------------|
| `clean` | Start cleaning in automatic mode. | |
| `spotArea` | Start cleaning specific rooms. | <ul><li>Only if supported by device, which can be recognized by `spotArea` being present in the list of possible states of the `current-cleaning-mode` channel.</li><li>Format: `spotArea:<room IDs>`, where `room IDs` is a semicolon separated list of room letters as shown in Ecovacs' app, so a valid command could e.g. be `spotArea:A;D;E`.</li><li>If you want to run 2 clean passes, amend `:x2` to the command, e.g. `spotArea:A;C;B:x2`.</li></ul> |
| `customArea` | Start cleaning specific areas. | <ul><li>Only if supported by device, which can be recognized by `customArea` being present in the list of possible states of the `current-cleaning-mode` channel.</li><li>Format: `customArea:<x1>;<y1>;<x2>;<y2>, where the parameters are coordinates (in mm) relative to the map.</li><li>The coordinates can be obtained from the `current-cleaning-spot-definition` channel when starting a custom area run from the app.</li><li>If you want to run 2 clean passes, amend `:x2` to the command, e.g. `customArea:100;100;1000;1000:x2`.</li></ul> |
| `pause` | Pause cleaning if it's currently active. | If the device is idle, the command is ignored. |
| `resume` | Resume cleaning if it's currently paused. | If the device is not paused, the command is ignored. |
| `stop` | Stop cleaning immediately. | |
| `charge` | Send device to charging station. | |
## Rule actions
This binding includes a rule action, which allows playback of specific sounds on the device in case the device has a speaker.
There is a separate instance for each device, which can be retrieved like this:
```java
val vacuumActions = getActions("ecovacs","ecovacs:vacuum:1234567890")
```
where the first parameter always has to be `ecovacs` and the second is the full Thing UID of the device that should be used.
Once this action instance is retrieved, you can invoke the `playSound(String type)` method on it:
```java
vacuumActions.playSound("beep")
```
Supported sound types include:
- `beep`
- `iAmHere`
- `startup`
- `suspended`
- `batteryLow`
For special use cases, there is also a `playSoundWithId(int soundId)` method, where you can pass the numeric ID of the sound to play.
The exact meaning of the number depends on the specific device; you'll need to experiment with different numbers to see how the number-to-sound mapping looks like.
For reference, a list for the Deebot 900 can be found [here](https://github.com/bmartin5692/sucks/blob/D901/protocol.md#user-content-sounds).
## File Based Configuration
If you want to create the API bridge in a .things file, the entry has to look as follows:
```java
Bridge ecovacs:ecovacsapi:ecovacsapi [ email="your.email@provider.com", password="yourpassword", continent="ww" ]
```
The possible values for `continent` include the following values:
- `ww` for World
- `eu` for Europe
- `na` for North America
- `as` for Asia
The devices are detected automatically.
If you also want to enter those manually, the syntax is as follows:
```java
Bridge ecovacs:ecovacsapi:ecovacsapi [ email="your.email@provider.com", password="yourpassword", continent="ww" ]
{
Thing vacuum myDeebot "Deebot Vacuum" [ serialNumber="serial as printed on label below dust bin" ]
}
```

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.ecovacs</artifactId>
<name>openHAB Add-ons :: Bundles :: Ecovacs Binding</name>
<properties>
<smack.version>4.3.3</smack.version>
</properties>
<dependencies>
<dependency>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-tcp</artifactId>
<version>${smack.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-im</artifactId>
<version>${smack.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-extensions</artifactId>
<version>${smack.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-java7</artifactId>
<version>${smack.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-resolver-javax</artifactId>
<version>${smack.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.ecovacs-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-ecovacs" description="Ecovacs Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature dependency="true">openhab.tp-hivemqclient</feature>
<bundle dependency="true">mvn:org.igniterealtime.smack/smack-tcp/4.3.3</bundle>
<bundle dependency="true">mvn:org.jxmpp/jxmpp-core/0.6.3</bundle>
<bundle dependency="true">mvn:org.jxmpp/jxmpp-jid/0.6.3</bundle>
<bundle dependency="true">mvn:org.jxmpp/jxmpp-util-cache/0.6.3</bundle>
<bundle dependency="true">mvn:org.minidns/minidns-core/0.3.3</bundle>
<bundle dependency="true">mvn:org.igniterealtime.smack/smack-core/4.3.3</bundle>
<bundle dependency="true">mvn:org.igniterealtime.smack/smack-im/4.3.3</bundle>
<bundle dependency="true">mvn:org.igniterealtime.smack/smack-extensions/4.3.3</bundle>
<bundle dependency="true">mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.xpp3/1.1.4c_7</bundle>
<bundle start-level="80">mvn:org.igniterealtime.smack/smack-resolver-javax/4.3.3</bundle>
<bundle start-level="80">mvn:org.igniterealtime.smack/smack-java7/4.3.3</bundle>
<bundle start-level="80">mvn:org.igniterealtime.smack/smack-sasl-javax/4.3.3</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.ecovacs/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,120 @@
/**
* Copyright (c) 2010-2023 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.commands.PlaySoundCommand.SoundType;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
import org.openhab.binding.ecovacs.internal.api.model.DeviceCapability;
import org.openhab.binding.ecovacs.internal.api.model.MoppingWaterAmount;
import org.openhab.binding.ecovacs.internal.api.model.SuctionPower;
import org.openhab.binding.ecovacs.internal.util.StateOptionEntry;
import org.openhab.binding.ecovacs.internal.util.StateOptionMapping;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link EcovacsBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class EcovacsBindingConstants {
private static final String BINDING_ID = "ecovacs";
// Client keys and secrets used for API authentication (extracted from Ecovacs app)
public static final String CLIENT_KEY = "1520391301804";
public static final String CLIENT_SECRET = "6c319b2a5cd3e66e39159c2e28f2fce9";
public static final String AUTH_CLIENT_KEY = "1520391491841";
public static final String AUTH_CLIENT_SECRET = "77ef58ce3afbe337da74aa8c5ab963a9";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_API = new ThingTypeUID(BINDING_ID, "ecovacsapi");
public static final ThingTypeUID THING_TYPE_VACUUM = new ThingTypeUID(BINDING_ID, "vacuum");
// List of all channel UIDs
public static final String CHANNEL_ID_AUTO_EMPTY = "settings#auto-empty";
public static final String CHANNEL_ID_BATTERY_LEVEL = "status#battery";
public static final String CHANNEL_ID_CLEANING_MODE = "status#current-cleaning-mode";
public static final String CHANNEL_ID_CLEANING_TIME = "status#current-cleaning-time";
public static final String CHANNEL_ID_CLEANED_AREA = "status#current-cleaned-area";
public static final String CHANNEL_ID_CLEANING_PASSES = "settings#cleaning-passes";
public static final String CHANNEL_ID_CLEANING_SPOT_DEFINITION = "status#current-cleaning-spot-definition";
public static final String CHANNEL_ID_CONTINUOUS_CLEANING = "settings#continuous-cleaning";
public static final String CHANNEL_ID_COMMAND = "actions#command";
public static final String CHANNEL_ID_DUST_FILTER_LIFETIME = "consumables#dust-filter-lifetime";
public static final String CHANNEL_ID_ERROR_CODE = "status#error-code";
public static final String CHANNEL_ID_ERROR_DESCRIPTION = "status#error-description";
public static final String CHANNEL_ID_LAST_CLEAN_START = "last-clean#last-clean-start";
public static final String CHANNEL_ID_LAST_CLEAN_DURATION = "last-clean#last-clean-duration";
public static final String CHANNEL_ID_LAST_CLEAN_AREA = "last-clean#last-clean-area";
public static final String CHANNEL_ID_LAST_CLEAN_MODE = "last-clean#last-clean-mode";
public static final String CHANNEL_ID_LAST_CLEAN_MAP = "last-clean#last-clean-map";
public static final String CHANNEL_ID_MAIN_BRUSH_LIFETIME = "consumables#main-brush-lifetime";
public static final String CHANNEL_ID_OTHER_COMPONENT_LIFETIME = "consumables#other-component-lifetime";
public static final String CHANNEL_ID_SIDE_BRUSH_LIFETIME = "consumables#side-brush-lifetime";
public static final String CHANNEL_ID_STATE = "status#state";
public static final String CHANNEL_ID_SUCTION_POWER = "settings#suction-power";
public static final String CHANNEL_ID_TOTAL_CLEANING_TIME = "total-stats#total-cleaning-time";
public static final String CHANNEL_ID_TOTAL_CLEANED_AREA = "total-stats#total-cleaned-area";
public static final String CHANNEL_ID_TOTAL_CLEAN_RUNS = "total-stats#total-clean-runs";
public static final String CHANNEL_ID_TRUE_DETECT_3D = "settings#true-detect-3d";
public static final String CHANNEL_ID_VOICE_VOLUME = "settings#voice-volume";
public static final String CHANNEL_ID_WATER_PLATE_PRESENT = "status#water-system-present";
public static final String CHANNEL_ID_WATER_AMOUNT = "settings#water-amount";
public static final String CHANNEL_ID_WIFI_RSSI = "status#wifi-rssi";
public static final String CHANNEL_TYPE_ID_CLEAN_MODE = "current-cleaning-mode";
public static final String CHANNEL_TYPE_ID_LAST_CLEAN_MODE = "last-clean-mode";
public static final String CMD_AUTO_CLEAN = "clean";
public static final String CMD_PAUSE = "pause";
public static final String CMD_RESUME = "resume";
public static final String CMD_CHARGE = "charge";
public static final String CMD_STOP = "stop";
public static final String CMD_SPOT_AREA = "spotArea";
public static final String CMD_CUSTOM_AREA = "customArea";
public static final StateOptionMapping<CleanMode> CLEAN_MODE_MAPPING = StateOptionMapping.<CleanMode> of(
new StateOptionEntry<CleanMode>(CleanMode.AUTO, "auto"),
new StateOptionEntry<CleanMode>(CleanMode.EDGE, "edge", DeviceCapability.EDGE_CLEANING),
new StateOptionEntry<CleanMode>(CleanMode.SPOT, "spot", DeviceCapability.SPOT_CLEANING),
new StateOptionEntry<CleanMode>(CleanMode.SPOT_AREA, "spotArea", DeviceCapability.SPOT_AREA_CLEANING),
new StateOptionEntry<CleanMode>(CleanMode.CUSTOM_AREA, "customArea", DeviceCapability.CUSTOM_AREA_CLEANING),
new StateOptionEntry<CleanMode>(CleanMode.SINGLE_ROOM, "singleRoom", DeviceCapability.SINGLE_ROOM_CLEANING),
new StateOptionEntry<CleanMode>(CleanMode.PAUSE, "pause"),
new StateOptionEntry<CleanMode>(CleanMode.STOP, "stop"),
new StateOptionEntry<CleanMode>(CleanMode.WASHING, "washing"),
new StateOptionEntry<CleanMode>(CleanMode.DRYING, "drying"),
new StateOptionEntry<CleanMode>(CleanMode.RETURNING, "returning"));
public static final StateOptionMapping<MoppingWaterAmount> WATER_AMOUNT_MAPPING = StateOptionMapping
.<MoppingWaterAmount> of(new StateOptionEntry<MoppingWaterAmount>(MoppingWaterAmount.LOW, "low"),
new StateOptionEntry<MoppingWaterAmount>(MoppingWaterAmount.MEDIUM, "medium"),
new StateOptionEntry<MoppingWaterAmount>(MoppingWaterAmount.HIGH, "high"),
new StateOptionEntry<MoppingWaterAmount>(MoppingWaterAmount.VERY_HIGH, "veryhigh"));
public static final StateOptionMapping<SuctionPower> SUCTION_POWER_MAPPING = StateOptionMapping.<SuctionPower> of(
new StateOptionEntry<SuctionPower>(SuctionPower.SILENT, "silent",
DeviceCapability.EXTENDED_CLEAN_SPEED_CONTROL),
new StateOptionEntry<SuctionPower>(SuctionPower.NORMAL, "normal"),
new StateOptionEntry<SuctionPower>(SuctionPower.HIGH, "high"), new StateOptionEntry<SuctionPower>(
SuctionPower.HIGHER, "higher", DeviceCapability.EXTENDED_CLEAN_SPEED_CONTROL));
public static final StateOptionMapping<SoundType> SOUND_TYPE_MAPPING = StateOptionMapping.<SoundType> of(
new StateOptionEntry<SoundType>(SoundType.BEEP, "beep"),
new StateOptionEntry<SoundType>(SoundType.I_AM_HERE, "iAmHere"),
new StateOptionEntry<SoundType>(SoundType.STARTUP, "startup"),
new StateOptionEntry<SoundType>(SoundType.SUSPENDED, "suspended"),
new StateOptionEntry<SoundType>(SoundType.BATTERY_LOW, "batteryLow"));
}

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2023 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;
import static org.openhab.binding.ecovacs.internal.EcovacsBindingConstants.*;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateOption;
import org.osgi.framework.Bundle;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicStateDescriptionProvider.class, EcovacsDynamicStateDescriptionProvider.class })
public class EcovacsDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
private final TranslationProvider i18nProvider;
@Activate
public EcovacsDynamicStateDescriptionProvider(final @Reference EventPublisher eventPublisher,
final @Reference TranslationProvider i18nProvider,
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.i18nProvider = i18nProvider;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
@Override
protected List<StateOption> localizedStateOptions(List<StateOption> options, Channel channel,
@Nullable Locale locale) {
@Nullable
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
String channelTypeId = channelTypeUID != null ? channelTypeUID.getId() : "";
if (CHANNEL_TYPE_ID_CLEAN_MODE.equals(channelTypeId) || CHANNEL_TYPE_ID_LAST_CLEAN_MODE.equals(channelTypeId)) {
final Bundle bundle = bundleContext.getBundle();
return options.stream().map(opt -> {
String key = "ecovacs.cleaning-mode." + opt.getValue();
String label = this.i18nProvider.getText(bundle, key, opt.getLabel(), locale);
return new StateOption(opt.getValue(), label);
}).collect(Collectors.toList());
}
return super.localizedStateOptions(options, channel, locale);
}
}

View File

@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2023 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;
import static org.openhab.binding.ecovacs.internal.EcovacsBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.handler.EcovacsApiHandler;
import org.openhab.binding.ecovacs.internal.handler.EcovacsVacuumHandler;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link EcovacsHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.ecovacs", service = ThingHandlerFactory.class)
public class EcovacsHandlerFactory extends BaseThingHandlerFactory {
private final HttpClientFactory httpClientFactory;
private final LocaleProvider localeProvider;
private final TranslationProvider i18Provider;
private final EcovacsDynamicStateDescriptionProvider stateDescriptionProvider;
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_API, THING_TYPE_VACUUM);
@Activate
public EcovacsHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
final @Reference EcovacsDynamicStateDescriptionProvider stateDescriptionProvider,
final @Reference LocaleProvider localeProvider, final @Reference TranslationProvider i18Provider) {
this.httpClientFactory = httpClientFactory;
this.stateDescriptionProvider = stateDescriptionProvider;
this.localeProvider = localeProvider;
this.i18Provider = i18Provider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_API.equals(thingTypeUID)) {
return new EcovacsApiHandler((Bridge) thing, httpClientFactory.getCommonHttpClient(), localeProvider);
} else {
return new EcovacsVacuumHandler(thing, i18Provider, localeProvider, stateDescriptionProvider);
}
}
}

View File

@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2023 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.action;
import static org.openhab.binding.ecovacs.internal.EcovacsBindingConstants.*;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.commands.PlaySoundCommand;
import org.openhab.binding.ecovacs.internal.handler.EcovacsVacuumHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Danny Baumann - Initial contribution
*/
@ThingActionsScope(name = "ecovacs")
@NonNullByDefault
public class EcovacsVacuumActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(EcovacsVacuumActions.class);
private @Nullable EcovacsVacuumHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (EcovacsVacuumHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@RuleAction(label = "@text/playSoundActionLabel", description = "@text/playSoundActionDesc")
public void playSound(
@ActionInput(name = "type", label = "@text/actionInputSoundTypeLabel", description = "@text/actionInputSoundTypeDesc") String type) {
EcovacsVacuumHandler handler = this.handler;
if (handler != null) {
Optional<PlaySoundCommand.SoundType> soundType = SOUND_TYPE_MAPPING.findMappedEnumValue(type);
if (soundType.isPresent()) {
handler.playSound(new PlaySoundCommand(soundType.get()));
} else {
logger.debug("Sound type '{}' is unknown, ignoring", type);
}
}
}
@RuleAction(label = "@text/playSoundActionLabel", description = "@text/playSoundActionDesc")
public void playSoundWithId(
@ActionInput(name = "soundId", label = "@text/actionInputSoundIdLabel", description = "@text/actionInputSoundIdDesc") int soundId) {
EcovacsVacuumHandler handler = this.handler;
if (handler != null) {
handler.playSound(new PlaySoundCommand(soundId));
}
}
public static void playSound(@Nullable ThingActions actions, String type) {
if (actions instanceof EcovacsVacuumActions) {
((EcovacsVacuumActions) actions).playSound(type);
}
}
public static void playSoundWithId(@Nullable ThingActions actions, int soundId) {
if (actions instanceof EcovacsVacuumActions) {
((EcovacsVacuumActions) actions).playSoundWithId(soundId);
}
}
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2023 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;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.ecovacs.internal.api.impl.EcovacsApiImpl;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public interface EcovacsApi {
public static EcovacsApi create(HttpClient httpClient, EcovacsApiConfiguration configuration) {
return new EcovacsApiImpl(httpClient, configuration);
}
public void loginAndGetAccessToken() throws EcovacsApiException, InterruptedException;
public List<EcovacsDevice> getDevices() throws EcovacsApiException, InterruptedException;
}

View File

@ -0,0 +1,140 @@
/**
* Copyright (c) 2010-2023 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.util.MD5Util;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
@NonNullByDefault
public final class EcovacsApiConfiguration {
private final String deviceId;
private final String username;
private final String password;
private final String continent;
private final String country;
private final String language;
private final String clientKey;
private final String clientSecret;
private final String authClientKey;
private final String authClientSecret;
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);
this.username = username;
this.password = password;
this.continent = continent;
this.country = country;
this.language = language;
this.clientKey = clientKey;
this.clientSecret = clientSecret;
this.authClientKey = authClientKey;
this.authClientSecret = authClientSecret;
}
public String getDeviceId() {
return deviceId;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getContinent() {
return continent;
}
public String getCountry() {
if ("gb".equalsIgnoreCase(country)) {
// United Kingdom's ISO 3166 abbreviation is 'gb', but Ecovacs wants the TLD instead, which is 'uk' for
// historical reasons
return "uk";
}
return country.toLowerCase();
}
public String getLanguage() {
return language;
}
public String getResource() {
return deviceId.substring(0, 8);
}
public String getAuthOpenId() {
return "global";
}
public String getTimeZone() {
return "GMT-8";
}
public String getRealm() {
return "ecouser.net";
}
public String getPortalAUthRequestWith() {
return "users";
}
public String getOrg() {
return "ECOWW";
}
public String getEdition() {
return "ECOGLOBLE";
}
public String getBizType() {
return "ECOVACS_IOT";
}
public String getChannel() {
return "google_play";
}
public String getAppCode() {
return "global_e";
}
public String getAppVersion() {
return "1.6.3";
}
public String getDeviceType() {
return "1";
}
public String getClientKey() {
return clientKey;
}
public String getClientSecret() {
return clientSecret;
}
public String getAuthClientKey() {
return authClientKey;
}
public String getAuthClientSecret() {
return authClientSecret;
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2023 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Response;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class EcovacsApiException extends Exception {
private static final long serialVersionUID = -5903398729974682356L;
public final boolean isAuthFailure;
public EcovacsApiException(String reason) {
this(reason, false);
}
public EcovacsApiException(String reason, boolean isAuthFailure) {
super(reason);
this.isAuthFailure = isAuthFailure;
}
public EcovacsApiException(Response response) {
super("HTTP status " + response.getStatus());
isAuthFailure = response.getStatus() == 401;
}
public EcovacsApiException(Throwable cause) {
this(cause, false);
}
public EcovacsApiException(Throwable cause, boolean isAuthFailure) {
super(cause);
this.isAuthFailure = isAuthFailure;
}
}

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2023 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;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.commands.IotDeviceCommand;
import org.openhab.binding.ecovacs.internal.api.model.CleanLogRecord;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
import org.openhab.binding.ecovacs.internal.api.model.DeviceCapability;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public interface EcovacsDevice {
public interface EventListener {
void onFirmwareVersionChanged(EcovacsDevice device, String fwVersion);
void onBatteryLevelUpdated(EcovacsDevice device, int newLevelPercent);
void onChargingStateUpdated(EcovacsDevice device, boolean charging);
void onCleaningModeUpdated(EcovacsDevice device, CleanMode newMode, Optional<String> areaDefinition);
void onCleaningStatsUpdated(EcovacsDevice device, int cleanedArea, int cleaningTimeSeconds);
void onWaterSystemPresentUpdated(EcovacsDevice device, boolean present);
void onErrorReported(EcovacsDevice device, int errorCode);
void onEventStreamFailure(EcovacsDevice device, Throwable error);
}
String getSerialNumber();
String getModelName();
boolean hasCapability(DeviceCapability cap);
void connect(EventListener listener, ScheduledExecutorService scheduler)
throws EcovacsApiException, InterruptedException;
void disconnect(ScheduledExecutorService scheduler);
<T> T sendCommand(IotDeviceCommand<T> command) throws EcovacsApiException, InterruptedException;
List<CleanLogRecord> getCleanLogs() throws EcovacsApiException, InterruptedException;
}

View File

@ -0,0 +1,83 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
class AbstractAreaCleaningCommand extends AbstractNoResponseCommand {
private final String jsonTypeName;
private final String areaDefinition;
private final int cleanPasses;
AbstractAreaCleaningCommand(String jsonTypeName, String areaDefinition, int cleanPasses) {
this.jsonTypeName = jsonTypeName;
this.areaDefinition = areaDefinition;
this.cleanPasses = cleanPasses;
}
@Override
public String getName(ProtocolVersion version) {
switch (version) {
case XML:
return "Clean";
case JSON:
return "clean";
case JSON_V2:
return "clean_V2";
}
throw new AssertionError();
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
Element clean = doc.createElement("clean");
clean.setAttribute("act", "s");
clean.setAttribute("type", "SpotArea");
clean.setAttribute("speed", "standard");
clean.setAttribute("p", areaDefinition);
clean.setAttribute("deep", String.valueOf(cleanPasses));
ctl.appendChild(clean);
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("act", "start");
JsonObject payload = args;
if (version == ProtocolVersion.JSON_V2) {
JsonObject content = new JsonObject();
args.add("content", content);
payload = content;
payload.addProperty("value", this.areaDefinition);
payload.addProperty("donotClean", 0);
payload.addProperty("total", 0);
} else {
payload.addProperty("content", this.areaDefinition);
}
payload.addProperty("count", cleanPasses);
payload.addProperty("type", this.jsonTypeName);
return args;
}
}

View File

@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2023 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.commands;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
abstract class AbstractCleaningCommand extends AbstractNoResponseCommand {
private final String xmlAction;
private final String jsonAction;
private final Optional<CleanMode> mode;
protected AbstractCleaningCommand(String xmlAction, String jsonAction, @Nullable CleanMode mode) {
super();
this.xmlAction = xmlAction;
this.jsonAction = jsonAction;
this.mode = Optional.ofNullable(mode);
}
@Override
public String getName(ProtocolVersion version) {
switch (version) {
case XML:
return "Clean";
case JSON:
return "clean";
case JSON_V2:
return "clean_V2";
}
throw new AssertionError();
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
Element clean = doc.createElement("clean");
getCleanModeProperty(ProtocolVersion.XML).ifPresent(m -> clean.setAttribute("type", m));
clean.setAttribute("speed", "standard");
clean.setAttribute("act", xmlAction);
ctl.appendChild(clean);
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("act", jsonAction);
getCleanModeProperty(version).ifPresent(m -> {
JsonObject payload = args;
if (version == ProtocolVersion.JSON_V2) {
JsonObject content = new JsonObject();
args.add("content", content);
payload = content;
}
payload.addProperty("type", m);
});
return args;
}
private Optional<String> getCleanModeProperty(ProtocolVersion version) {
return mode.flatMap(m -> {
switch (m) {
case AUTO:
return Optional.of("auto");
case CUSTOM_AREA:
return Optional.of(version == ProtocolVersion.XML ? "CustomArea" : "customArea");
case EDGE:
return Optional.of("border");
case SPOT:
return Optional.of("spot");
case SPOT_AREA:
return Optional.of(version == ProtocolVersion.XML ? "SpotArea" : "spotArea");
case SINGLE_ROOM:
return Optional.of("singleRoom");
case STOP:
return Optional.of("stop");
default:
return Optional.empty();
}
});
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractNoResponseCommand extends IotDeviceCommand<AbstractNoResponseCommand.Nothing> {
public static class Nothing {
private Nothing() {
}
private static final Nothing INSTANCE = new Nothing();
}
protected AbstractNoResponseCommand() {
super();
}
@Override
public Nothing convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson) {
return Nothing.INSTANCE;
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class CustomAreaCleaningCommand extends AbstractAreaCleaningCommand {
public CustomAreaCleaningCommand(String areaDefinition, int cleanPasses) {
super("customArea", areaDefinition, cleanPasses);
}
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class EmptyDustbinCommand extends AbstractNoResponseCommand {
public EmptyDustbinCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
if (version == ProtocolVersion.XML) {
throw new IllegalStateException("Empty dust bin is not supported for XML");
}
return "setAutoEmpty";
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("act", "start");
return args;
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.CachedMapInfoReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.util.DataParsingException;
import org.openhab.binding.ecovacs.internal.api.util.XPathUtils;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetActiveMapIdCommand extends IotDeviceCommand<String> {
public GetActiveMapIdCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetMapM" : "getCachedMapInfo";
}
@Override
public String convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
CachedMapInfoReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
CachedMapInfoReport.class);
return resp.mapInfos.stream().filter(i -> i.used != 0).map(i -> i.mapId).findFirst().orElse("");
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return XPathUtils.getFirstXPathMatch(payload, "//@i").getNodeValue();
}
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.BatteryReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.DeviceInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.util.DataParsingException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetBatteryInfoCommand extends IotDeviceCommand<Integer> {
public GetBatteryInfoCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetBatteryInfo" : "getBattery";
}
@Override
public Integer convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
BatteryReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
BatteryReport.class);
return resp.percent;
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return DeviceInfo.parseBatteryInfo(payload);
}
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.ChargeReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.DeviceInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.model.ChargeMode;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetChargeStateCommand extends IotDeviceCommand<ChargeMode> {
public GetChargeStateCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetChargeState" : "getChargeState";
}
@Override
public ChargeMode convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
ChargeReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
ChargeReport.class);
return resp.isCharging != 0 ? ChargeMode.CHARGING : ChargeMode.IDLE;
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return DeviceInfo.parseChargeInfo(payload, gson);
}
}
}

View File

@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2023 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.commands;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandXmlResponse;
import org.openhab.binding.ecovacs.internal.api.model.CleanLogRecord;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetCleanLogsCommand extends IotDeviceCommand<List<CleanLogRecord>> {
private static final int LOG_SIZE = 20;
@Override
public String getName(ProtocolVersion version) {
if (version != ProtocolVersion.XML) {
throw new IllegalStateException("Command is only supported for XML");
}
return "GetCleanLogs";
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
ctl.setAttribute("count", String.valueOf(LOG_SIZE));
}
@Override
public List<CleanLogRecord> convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version,
Gson gson) throws DataParsingException {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder db = dbf.newDocumentBuilder();
NodeList entryNodes = db.parse(new ByteArrayInputStream(payload.getBytes("UTF-8"))).getFirstChild()
.getChildNodes();
List<CleanLogRecord> result = new ArrayList<>();
for (int i = 0; i < entryNodes.getLength(); i++) {
NamedNodeMap attrs = entryNodes.item(i).getAttributes();
String area = attrs.getNamedItem("a").getNodeValue();
String startTime = attrs.getNamedItem("s").getNodeValue();
String duration = attrs.getNamedItem("l").getNodeValue();
result.add(new CleanLogRecord(Long.parseLong(startTime), Integer.parseInt(duration),
Integer.parseInt(area), Optional.empty(), CleanMode.IDLE));
}
return result;
} catch (ParserConfigurationException | SAXException | IOException e) {
throw new DataParsingException(e);
}
}
}

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.CleanReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.CleanReportV2;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.CleaningInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.model.CleanMode;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetCleanStateCommand extends IotDeviceCommand<CleanMode> {
public GetCleanStateCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
switch (version) {
case XML:
return "GetCleanState";
case JSON:
return "getCleanInfo";
case JSON_V2:
return "getCleanInfo_V2";
}
throw new AssertionError();
}
@Override
public CleanMode convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
final PortalIotCommandJsonResponse jsonResponse = (PortalIotCommandJsonResponse) response;
final CleanMode mode;
if (version == ProtocolVersion.JSON) {
CleanReport resp = jsonResponse.getResponsePayloadAs(gson, CleanReport.class);
mode = resp.determineCleanMode(gson);
} else {
CleanReportV2 resp = jsonResponse.getResponsePayloadAs(gson, CleanReportV2.class);
mode = resp.determineCleanMode(gson);
}
if (mode == null) {
throw new DataParsingException("Could not get clean mode from response " + jsonResponse.response);
}
return mode;
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return CleaningInfo.parseCleanStateInfo(payload, gson).mode;
}
}
}

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2010-2023 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.commands;
import java.lang.reflect.Type;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.ComponentLifeSpanReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.DeviceInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.model.Component;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetComponentLifeSpanCommand extends IotDeviceCommand<Integer> {
private final Component type;
public GetComponentLifeSpanCommand(Component type) {
this.type = type;
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetLifeSpan" : "getLifeSpan";
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
ctl.setAttribute("type", type.xmlValue);
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonArray args = new JsonArray(1);
args.add(type.jsonValue);
return args;
}
@Override
public Integer convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
JsonElement respPayloadRaw = ((PortalIotCommandJsonResponse) response).getResponsePayload(gson);
Type type = new TypeToken<List<ComponentLifeSpanReport>>() {
}.getType();
try {
List<ComponentLifeSpanReport> resp = gson.fromJson(respPayloadRaw, type);
if (resp == null || resp.isEmpty()) {
throw new DataParsingException("Invalid lifespan response " + respPayloadRaw);
}
return (int) Math.round(100.0 * resp.get(0).left / resp.get(0).total);
} catch (JsonSyntaxException e) {
throw new DataParsingException(e);
}
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return DeviceInfo.parseComponentLifespanInfo(payload);
}
}
}

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.EnabledStateReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.DeviceInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.util.DataParsingException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetContinuousCleaningCommand extends IotDeviceCommand<Boolean> {
public GetContinuousCleaningCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetOnOff" : "getBreakPoint";
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
ctl.setAttribute("t", "g");
}
@Override
public Boolean convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
EnabledStateReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
EnabledStateReport.class);
return resp.enabled != 0;
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return DeviceInfo.parseEnabledStateInfo(payload);
}
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.DefaultCleanCountReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandJsonResponse;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetDefaultCleanPassesCommand extends IotDeviceCommand<Integer> {
public GetDefaultCleanPassesCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
if (version == ProtocolVersion.XML) {
throw new IllegalStateException("Command is not supported for XML");
}
return "getCleanCount";
}
@Override
public Integer convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
DefaultCleanCountReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
DefaultCleanCountReport.class);
return resp.count;
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.EnabledStateReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandJsonResponse;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetDustbinAutoEmptyCommand extends IotDeviceCommand<Boolean> {
public GetDustbinAutoEmptyCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
if (version == ProtocolVersion.XML) {
throw new IllegalStateException("Command is not supported for XML");
}
return "getAutoEmpty";
}
@Override
public Boolean convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
EnabledStateReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
EnabledStateReport.class);
return resp.enabled != 0;
}
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2023 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.commands;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.ErrorReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.DeviceInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.util.DataParsingException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetErrorCommand extends IotDeviceCommand<Optional<Integer>> {
public GetErrorCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetError" : "getError";
}
@Override
public Optional<Integer> convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version,
Gson gson) throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
ErrorReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson, ErrorReport.class);
if (resp.errorCodes.isEmpty()) {
return Optional.empty();
}
return Optional.of(resp.errorCodes.get(0));
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return DeviceInfo.parseErrorInfo(payload);
}
}
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandXmlResponse;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.openhab.binding.ecovacs.internal.api.util.XPathUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetFirmwareVersionCommand extends IotDeviceCommand<String> {
public GetFirmwareVersionCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
if (version != ProtocolVersion.XML) {
throw new IllegalStateException("Get FW version is only supported for XML");
}
return "GetVersion";
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
ctl.setAttribute("name", "FW");
}
@Override
public String convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return XPathUtils.getFirstXPathMatch(payload, "//ver[@name='FW']").getTextContent();
}
}

View File

@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2023 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.commands;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.MapSetReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.util.DataParsingException;
import org.openhab.binding.ecovacs.internal.api.util.XPathUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetMapSpotAreasWithMapIdCommand extends IotDeviceCommand<List<String>> {
private final String mapId;
public GetMapSpotAreasWithMapIdCommand(String mapId) {
this.mapId = mapId;
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetMapSet" : "getMapSet";
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
ctl.setAttribute("tp", "sa");
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("mid", mapId);
args.addProperty("type", "ar");
return args;
}
@Override
public List<String> convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
MapSetReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
MapSetReport.class);
return resp.subsets.stream().map(i -> i.id).collect(Collectors.toList());
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
NodeList mapIds = XPathUtils.getXPathMatches(payload, "//m/@mid");
List<String> result = new ArrayList<>();
for (int i = 0; i < mapIds.getLength(); i++) {
result.add(mapIds.item(i).getNodeValue());
}
return result;
}
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.WaterInfoReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.WaterSystemInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.model.MoppingWaterAmount;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetMoppingWaterAmountCommand extends IotDeviceCommand<MoppingWaterAmount> {
public GetMoppingWaterAmountCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetWaterPermeability" : "getWaterInfo";
}
@Override
public MoppingWaterAmount convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version,
Gson gson) throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
WaterInfoReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
WaterInfoReport.class);
return MoppingWaterAmount.fromApiValue(resp.waterAmount);
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return WaterSystemInfo.parseWaterPermeabilityInfo(payload);
}
}
}

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.NetworkInfoReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.model.NetworkInfo;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.openhab.binding.ecovacs.internal.api.util.XPathUtils;
import org.w3c.dom.Node;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetNetworkInfoCommand extends IotDeviceCommand<NetworkInfo> {
public GetNetworkInfoCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetNetInfo" : "getNetInfo";
}
@Override
public NetworkInfo convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
NetworkInfoReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
NetworkInfoReport.class);
try {
return new NetworkInfo(resp.ip, resp.mac, resp.ssid, Integer.valueOf(resp.rssi));
} catch (NumberFormatException e) {
throw new DataParsingException(e);
}
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
Node ipAttr = XPathUtils.getFirstXPathMatch(payload, "//@wi");
Node ssidAttr = XPathUtils.getFirstXPathMatch(payload, "//@s");
return new NetworkInfo(ipAttr.getNodeValue(), "", ssidAttr.getNodeValue(), 0);
}
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.SpeedReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.CleaningInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.model.SuctionPower;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetSuctionPowerCommand extends IotDeviceCommand<SuctionPower> {
public GetSuctionPowerCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetCleanSpeed" : "getSpeed";
}
@Override
public SuctionPower convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
SpeedReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson, SpeedReport.class);
return SuctionPower.fromJsonValue(resp.speedLevel);
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return CleaningInfo.parseCleanSpeedInfo(payload, gson);
}
}
}

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.util.DataParsingException;
import org.openhab.binding.ecovacs.internal.api.util.XPathUtils;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetTotalStatsCommand extends IotDeviceCommand<GetTotalStatsCommand.TotalStats> {
public class TotalStats {
@SerializedName("area")
public final int totalArea;
@SerializedName("time")
public final int totalRuntime;
@SerializedName("count")
public final int cleanRuns;
private TotalStats(int area, int runtime, int runs) {
this.totalArea = area;
this.totalRuntime = runtime;
this.cleanRuns = runs;
}
}
public GetTotalStatsCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetCleanSum" : "getTotalStats";
}
@Override
public TotalStats convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
return ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson, TotalStats.class);
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
String area = XPathUtils.getFirstXPathMatch(payload, "//@a").getNodeValue();
String time = XPathUtils.getFirstXPathMatch(payload, "//@l").getNodeValue();
String count = XPathUtils.getFirstXPathMatch(payload, "//@c").getNodeValue();
try {
return new TotalStats(Integer.valueOf(area), Integer.valueOf(time), Integer.valueOf(count));
} catch (NumberFormatException e) {
throw new DataParsingException(e);
}
}
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.EnabledStateReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandJsonResponse;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetTrueDetectCommand extends IotDeviceCommand<Boolean> {
public GetTrueDetectCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
if (version == ProtocolVersion.XML) {
throw new IllegalStateException("Command is not supported for XML");
}
return "getTrueDetect";
}
@Override
public Boolean convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
EnabledStateReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
EnabledStateReport.class);
return resp.enabled != 0;
}
}

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandJsonResponse;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetVolumeCommand extends IotDeviceCommand<Integer> {
public GetVolumeCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
if (version == ProtocolVersion.XML) {
throw new IllegalStateException("Get volume command is not supported for XML");
}
return "getVolume";
}
@Override
public Integer convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
JsonResponse resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
JsonResponse.class);
return resp.volume;
} else {
// unsupported in XML case?
return 0;
}
}
private static class JsonResponse {
@SerializedName("volume")
public int volume;
@SerializedName("total")
public int maxVolume;
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.WaterInfoReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.WaterSystemInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
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.util.DataParsingException;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GetWaterSystemPresentCommand extends IotDeviceCommand<Boolean> {
public GetWaterSystemPresentCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "GetWaterBoxInfo" : "getWaterInfo";
}
@Override
public Boolean convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
throws DataParsingException {
if (response instanceof PortalIotCommandJsonResponse) {
WaterInfoReport resp = ((PortalIotCommandJsonResponse) response).getResponsePayloadAs(gson,
WaterInfoReport.class);
return resp.waterPlatePresent != 0;
} else {
String payload = ((PortalIotCommandXmlResponse) response).getResponsePayloadXml();
return WaterSystemInfo.parseWaterBoxInfo(payload);
}
}
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class GoChargingCommand extends AbstractNoResponseCommand {
public GoChargingCommand() {
super();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "Charge" : "charge";
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
Element charge = doc.createElement("charge");
charge.setAttribute("type", "go");
ctl.appendChild(charge);
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("act", "go");
return args;
}
}

View File

@ -0,0 +1,87 @@
/**
* Copyright (c) 2010-2023 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.commands;
import java.io.StringWriter;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.impl.dto.request.portal.PortalIotCommandRequest.JsonPayloadHeader;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public abstract class IotDeviceCommand<RESPONSETYPE> {
protected IotDeviceCommand() {
}
public abstract String getName(ProtocolVersion version);
public final String getXmlPayload(@Nullable String id) throws ParserConfigurationException, TransformerException {
Document xmlDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element ctl = xmlDoc.createElement("ctl");
ctl.setAttribute("td", getName(ProtocolVersion.XML));
if (id != null) {
ctl.setAttribute("id", id);
}
applyXmlPayload(xmlDoc, ctl);
xmlDoc.appendChild(ctl);
Transformer tf = TransformerFactory.newInstance().newTransformer();
tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
StringWriter writer = new StringWriter();
tf.transform(new DOMSource(xmlDoc), new StreamResult(writer));
return writer.getBuffer().toString().replaceAll("\n|\r", "");
}
public final JsonElement getJsonPayload(ProtocolVersion version, Gson gson) {
JsonObject result = new JsonObject();
result.add("header", gson.toJsonTree(new JsonPayloadHeader()));
@Nullable
JsonElement args = getJsonPayloadArgs(version);
if (args != null) {
JsonObject body = new JsonObject();
body.add("data", args);
result.add("body", body);
}
return result;
}
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
return null;
}
protected void applyXmlPayload(Document doc, Element ctl) {
}
public abstract RESPONSETYPE convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version,
Gson gson) throws DataParsingException;
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class PauseCleaningCommand extends AbstractCleaningCommand {
public PauseCleaningCommand(CleanMode mode) {
super("p", "pause", mode);
}
}

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class PlaySoundCommand extends AbstractNoResponseCommand {
public enum SoundType {
STARTUP(0),
SUSPENDED(3),
CHECK_WHEELS(4),
HELP_ME_OUT(5),
INSTALL_DUST_BIN(6),
BEEP(17),
BATTERY_LOW(18),
POWER_ON_BEFORE_CHARGE(29),
I_AM_HERE(30),
PLEASE_CLEAN_BRUSH(31),
PLEASE_CLEAN_SENSORS(35),
BRUSH_IS_TANGLED(48),
RELOCATING(55),
UPGRADE_DONE(56),
RETURNING_TO_CHARGE(63),
CLEANING_PAUSED(65),
CONNECTED_IN_SETUP(69),
RESTORING_MAP(71),
BATTERY_LOW_RETURNING_TO_DOCK(73),
DIFFICULT_TO_LOCATE(74),
RESUMING_CLEANING(75),
UPGRADE_FAILED(76),
PLACE_ON_CHARGING_DOCK(77),
RESUME_CLEANING(79),
STARTING_CLEANING(80),
READY_FOR_MOPPING(84),
REMOVE_MOPPING_PLATE(85),
CLEANING_COMPLETE(86),
LDS_MALFUNCTION(89),
UPGRADING(90);
final int id;
private SoundType(int id) {
this.id = id;
}
}
private final int soundId;
public PlaySoundCommand(SoundType type) {
super();
this.soundId = type.id;
}
public PlaySoundCommand(int soundId) {
super();
this.soundId = soundId;
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "PlaySound" : "playSound";
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
ctl.setAttribute("sid", String.valueOf(soundId));
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("sid", soundId);
return args;
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class ResumeCleaningCommand extends AbstractCleaningCommand {
public ResumeCleaningCommand(CleanMode mode) {
super("r", "resume", mode);
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class SetContinuousCleaningCommand extends AbstractNoResponseCommand {
private final boolean enabled;
public SetContinuousCleaningCommand(boolean enabled) {
super();
this.enabled = enabled;
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "SetOnOff" : "setBreakPoint";
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
ctl.setAttribute("t", "g");
ctl.setAttribute("on", enabled ? "1" : "0");
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("enable", enabled ? 1 : 0);
return args;
}
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class SetDefaultCleanPassesCommand extends AbstractNoResponseCommand {
private final int count;
public SetDefaultCleanPassesCommand(int count) {
if (count < 1 || count > 2) {
throw new IllegalArgumentException("Number of cleaning passes must be between 1 and 2");
}
this.count = count;
}
@Override
public String getName(ProtocolVersion version) {
if (version == ProtocolVersion.XML) {
throw new IllegalStateException("Set default clean count is not supported for XML");
}
return "setCleanCount";
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("count", count);
return args;
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class SetDustbinAutoEmptyCommand extends AbstractNoResponseCommand {
private final boolean on;
public SetDustbinAutoEmptyCommand(boolean on) {
this.on = on;
}
@Override
public String getName(ProtocolVersion version) {
if (version == ProtocolVersion.XML) {
throw new IllegalStateException("Set dust bin auto empty is not supported for XML");
}
return "setAutoEmpty";
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("enable", on ? 1 : 0);
return args;
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.model.MoppingWaterAmount;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class SetMoppingWaterAmountCommand extends AbstractNoResponseCommand {
private final int level;
public SetMoppingWaterAmountCommand(MoppingWaterAmount amount) {
super();
this.level = amount.toApiValue();
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "SetWaterPermeability" : "setWaterInfo";
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
ctl.setAttribute("v", String.valueOf(level));
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("amount", level);
return args;
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import org.openhab.binding.ecovacs.internal.api.model.SuctionPower;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class SetSuctionPowerCommand extends AbstractNoResponseCommand {
private final SuctionPower power;
public SetSuctionPowerCommand(SuctionPower power) {
this.power = power;
}
@Override
public String getName(ProtocolVersion version) {
return version == ProtocolVersion.XML ? "SetCleanSpeed" : "setSpeed";
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("speed", power.toJsonValue());
return args;
}
@Override
protected void applyXmlPayload(Document doc, Element ctl) {
ctl.setAttribute("speed", power.toXmlValue());
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class SetTrueDetectCommand extends AbstractNoResponseCommand {
private final boolean on;
public SetTrueDetectCommand(boolean on) {
this.on = on;
}
@Override
public String getName(ProtocolVersion version) {
if (version == ProtocolVersion.XML) {
throw new IllegalStateException("Set true detect is not supported for XML");
}
return "setTrueDetect";
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("enable", on ? 1 : 0);
return args;
}
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class SetVolumeCommand extends AbstractNoResponseCommand {
private final int volume;
public SetVolumeCommand(int volume) {
if (volume < 0 || volume > 10) {
throw new IllegalArgumentException("Volume must be between 0 and 10");
}
this.volume = volume;
}
@Override
public String getName(ProtocolVersion version) {
if (version == ProtocolVersion.XML) {
throw new IllegalStateException("Set volume is not supported for XML");
}
return "setVolume";
}
@Override
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
JsonObject args = new JsonObject();
args.addProperty("volume", volume);
return args;
}
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2023 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.commands;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class SpotAreaCleaningCommand extends AbstractAreaCleaningCommand {
public SpotAreaCleaningCommand(List<String> roomIds, int cleanPasses) {
super("spotArea", String.join(",", roomIds), cleanPasses);
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class StartAutoCleaningCommand extends AbstractCleaningCommand {
public StartAutoCleaningCommand() {
super("s", "start", CleanMode.AUTO);
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2023 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.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class StopCleaningCommand extends AbstractCleaningCommand {
public StopCleaningCommand() {
super("h", "stop", CleanMode.STOP);
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2023 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;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.model.DeviceCapability;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class DeviceDescription {
public final String modelName;
public final String deviceClass;
public final @Nullable String deviceClassLink;
public final ProtocolVersion protoVersion;
public final boolean usesMqtt;
public final Set<DeviceCapability> capabilities;
public DeviceDescription(String modelName, String deviceClass, @Nullable String deviceClassLink,
ProtocolVersion protoVersion, boolean usesMqtt, Set<DeviceCapability> capabilities) {
this.modelName = modelName;
this.capabilities = capabilities;
this.deviceClass = deviceClass;
this.deviceClassLink = deviceClassLink;
this.protoVersion = protoVersion;
this.usesMqtt = usesMqtt;
}
public DeviceDescription resolveLinkWith(DeviceDescription other) {
return new DeviceDescription(modelName, deviceClass, null, other.protoVersion, other.usesMqtt,
other.capabilities);
}
public void addImplicitCapabilities() {
if (protoVersion != ProtocolVersion.XML && capabilities.contains(DeviceCapability.CLEAN_SPEED_CONTROL)) {
capabilities.add(DeviceCapability.EXTENDED_CLEAN_SPEED_CONTROL);
}
if (protoVersion != ProtocolVersion.XML) {
capabilities.add(DeviceCapability.EXTENDED_CLEAN_LOG_RECORD);
}
if (!capabilities.contains(DeviceCapability.SPOT_AREA_CLEANING)) {
capabilities.add(DeviceCapability.EDGE_CLEANING);
capabilities.add(DeviceCapability.SPOT_CLEANING);
}
if (protoVersion == ProtocolVersion.JSON_V2) {
capabilities.add(DeviceCapability.DEFAULT_CLEAN_COUNT_SETTING);
}
}
}

View File

@ -0,0 +1,361 @@
/**
* Copyright (c) 2010-2023 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;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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;
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;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.ecovacs.internal.api.EcovacsApi;
import org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration;
import org.openhab.binding.ecovacs.internal.api.EcovacsApiException;
import org.openhab.binding.ecovacs.internal.api.EcovacsDevice;
import org.openhab.binding.ecovacs.internal.api.commands.IotDeviceCommand;
import org.openhab.binding.ecovacs.internal.api.impl.dto.request.portal.PortalAuthRequest;
import org.openhab.binding.ecovacs.internal.api.impl.dto.request.portal.PortalAuthRequestParameter;
import org.openhab.binding.ecovacs.internal.api.impl.dto.request.portal.PortalCleanLogsRequest;
import org.openhab.binding.ecovacs.internal.api.impl.dto.request.portal.PortalIotCommandRequest;
import org.openhab.binding.ecovacs.internal.api.impl.dto.request.portal.PortalIotProductRequest;
import org.openhab.binding.ecovacs.internal.api.impl.dto.request.portal.PortalLoginRequest;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.main.AccessData;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.main.AuthCode;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.main.ResponseWrapper;
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.PortalCleanLogsResponse;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
/**
* @author Danny Baumann - Initial contribution
* @author Johannes Ptaszyk - Initial contribution
*/
@NonNullByDefault
public final class EcovacsApiImpl implements EcovacsApi {
private final Logger logger = LoggerFactory.getLogger(EcovacsApiImpl.class);
private final HttpClient httpClient;
private final Gson gson = new Gson();
private final EcovacsApiConfiguration configuration;
private @Nullable PortalLoginResponse loginData;
public EcovacsApiImpl(HttpClient httpClient, EcovacsApiConfiguration configuration) {
this.httpClient = httpClient;
this.configuration = configuration;
}
@Override
public void loginAndGetAccessToken() throws EcovacsApiException, InterruptedException {
loginData = null;
AccessData accessData = login();
AuthCode authCode = getAuthCode(accessData);
loginData = portalLogin(authCode, accessData);
}
EcovacsApiConfiguration getConfig() {
return configuration;
}
@Nullable
PortalLoginResponse getLoginData() {
return loginData;
}
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("authTimeZone", configuration.getTimeZone());
loginParameters.put("country", configuration.getCountry());
loginParameters.put("lang", configuration.getLanguage());
loginParameters.put("deviceId", configuration.getDeviceId());
loginParameters.put("appCode", configuration.getAppCode());
loginParameters.put("appVersion", configuration.getAppVersion());
loginParameters.put("channel", configuration.getChannel());
loginParameters.put("deviceType", configuration.getDeviceType());
Request loginRequest = createAuthRequest(EcovacsApiUrlFactory.getLoginUrl(configuration),
configuration.getClientKey(), configuration.getClientSecret(), loginParameters);
ContentResponse loginResponse = executeRequest(loginRequest);
Type responseType = new TypeToken<ResponseWrapper<AccessData>>() {
}.getType();
return handleResponseWrapper(gson.fromJson(loginResponse.getContentAsString(), responseType));
}
private AuthCode getAuthCode(AccessData accessData) throws EcovacsApiException, InterruptedException {
HashMap<String, String> authCodeParameters = new HashMap<>();
authCodeParameters.put("uid", accessData.getUid());
authCodeParameters.put("accessToken", accessData.getAccessToken());
authCodeParameters.put("bizType", configuration.getBizType());
authCodeParameters.put("deviceId", configuration.getDeviceId());
authCodeParameters.put("openId", configuration.getAuthOpenId());
Request authCodeRequest = createAuthRequest(EcovacsApiUrlFactory.getAuthUrl(configuration),
configuration.getAuthClientKey(), configuration.getAuthClientSecret(), authCodeParameters);
ContentResponse authCodeResponse = executeRequest(authCodeRequest);
Type responseType = new TypeToken<ResponseWrapper<AuthCode>>() {
}.getType();
return handleResponseWrapper(gson.fromJson(authCodeResponse.getContentAsString(), responseType));
}
private PortalLoginResponse portalLogin(AuthCode authCode, AccessData accessData)
throws EcovacsApiException, InterruptedException {
PortalLoginRequest loginRequestData = new PortalLoginRequest(PortalTodo.LOGIN_BY_TOKEN,
configuration.getCountry().toUpperCase(), "", configuration.getOrg(), configuration.getResource(),
configuration.getRealm(), authCode.getAuthCode(), accessData.getUid(), configuration.getEdition());
String userUrl = EcovacsApiUrlFactory.getPortalUsersUrl(configuration);
ContentResponse portalLoginResponse = executeRequest(createJsonRequest(userUrl, loginRequestData));
PortalLoginResponse response = handleResponse(portalLoginResponse, PortalLoginResponse.class);
if (!response.wasSuccessful()) {
throw new EcovacsApiException("Login failed");
}
return response;
}
@Override
public List<EcovacsDevice> getDevices() throws EcovacsApiException, InterruptedException {
List<DeviceDescription> descriptions = getSupportedDeviceList();
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();
if (!descOpt.isPresent()) {
if (products == null) {
products = getIotProductMap();
}
String modelName = products.stream().filter(prod -> dev.getDeviceClass().equals(prod.getClassId()))
.findFirst().map(p -> p.getDefinition().name).orElse("UNKNOWN");
logger.info("Found unsupported device {} (class {}, company {}), ignoring.", modelName,
dev.getDeviceClass(), dev.getCompany());
continue;
}
DeviceDescription desc = descOpt.get();
if (desc.usesMqtt) {
devices.add(new EcovacsIotMqDevice(dev, desc, this, gson));
} else {
devices.add(new EcovacsXmppDevice(dev, desc, this, gson));
}
}
return devices;
}
private List<DeviceDescription> getSupportedDeviceList() {
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;
if (desc.deviceClassLink != null) {
Optional<DeviceDescription> linkedDescOpt = descs.stream()
.filter(d -> d.deviceClass.equals(desc.deviceClassLink)).findFirst();
if (!linkedDescOpt.isPresent()) {
throw new IllegalStateException(
"Desc " + desc.deviceClass + " links unknown desc " + desc.deviceClassLink);
}
result = desc.resolveLinkWith(linkedDescOpt.get());
} else {
result = desc;
}
result.addImplicitCapabilities();
return result;
}).collect(Collectors.toList());
}
private List<Device> getDeviceList() throws EcovacsApiException, InterruptedException {
PortalAuthRequest data = new PortalAuthRequest(PortalTodo.GET_DEVICE_LIST, createAuthData());
String userUrl = EcovacsApiUrlFactory.getPortalUsersUrl(configuration);
ContentResponse deviceResponse = executeRequest(createJsonRequest(userUrl, data));
logger.trace("Got device list response {}", deviceResponse.getContentAsString());
List<Device> devices = handleResponse(deviceResponse, PortalDeviceResponse.class).getDevices();
return devices != null ? devices : Collections.emptyList();
}
private List<IotProduct> getIotProductMap() throws EcovacsApiException, InterruptedException {
PortalIotProductRequest data = new PortalIotProductRequest(createAuthData());
String url = EcovacsApiUrlFactory.getPortalProductIotMapUrl(configuration);
ContentResponse productResponse = executeRequest(createJsonRequest(url, data));
logger.trace("Got product list response {}", productResponse.getContentAsString());
List<IotProduct> products = handleResponse(productResponse, PortalIotProductResponse.class).getProducts();
return products != null ? products : Collections.emptyList();
}
public <T> T sendIotCommand(Device device, DeviceDescription desc, IotDeviceCommand<T> command)
throws EcovacsApiException, InterruptedException {
String commandName = command.getName(desc.protoVersion);
final Object payload;
try {
if (desc.protoVersion == ProtocolVersion.XML) {
payload = command.getXmlPayload(null);
logger.trace("{}: Sending IOT command {} with payload {}", device.getName(), commandName, payload);
} else {
payload = command.getJsonPayload(desc.protoVersion, gson);
logger.trace("{}: Sending IOT command {} with payload {}", device.getName(), commandName,
gson.toJson(payload));
}
} catch (ParserConfigurationException | TransformerException e) {
logger.debug("Could not convert payload for {}", command, e);
throw new EcovacsApiException(e);
}
PortalIotCommandRequest data = new PortalIotCommandRequest(createAuthData(), commandName, payload,
device.getDid(), device.getResource(), device.getDeviceClass(),
desc.protoVersion != ProtocolVersion.XML);
String url = EcovacsApiUrlFactory.getPortalIotDeviceManagerUrl(configuration);
ContentResponse response = executeRequest(createJsonRequest(url, data));
final AbstractPortalIotCommandResponse commandResponse;
if (desc.protoVersion == ProtocolVersion.XML) {
commandResponse = handleResponse(response, PortalIotCommandXmlResponse.class);
logger.trace("{}: Got response payload {}", device.getName(),
((PortalIotCommandXmlResponse) commandResponse).getResponsePayloadXml());
} else {
commandResponse = handleResponse(response, PortalIotCommandJsonResponse.class);
logger.trace("{}: Got response payload {}", device.getName(),
((PortalIotCommandJsonResponse) commandResponse).response);
}
if (!commandResponse.wasSuccessful()) {
final String msg = "Sending IOT command " + commandName + " failed: " + commandResponse.getErrorMessage();
throw new EcovacsApiException(msg, commandResponse.failedDueToAuthProblem());
}
try {
return command.convertResponse(commandResponse, desc.protoVersion, gson);
} catch (DataParsingException e) {
logger.debug("Converting response for command {} failed", command, e);
throw new EcovacsApiException(e);
}
}
public List<PortalCleanLogsResponse.LogRecord> fetchCleanLogs(Device device)
throws EcovacsApiException, InterruptedException {
PortalCleanLogsRequest data = new PortalCleanLogsRequest(createAuthData(), device.getDid(),
device.getResource());
String url = EcovacsApiUrlFactory.getPortalLogUrl(configuration);
ContentResponse response = executeRequest(createJsonRequest(url, data));
PortalCleanLogsResponse responseObj = handleResponse(response, PortalCleanLogsResponse.class);
if (!responseObj.wasSuccessful()) {
throw new EcovacsApiException("Fetching clean logs failed");
}
logger.trace("{}: Fetching cleaning logs yields {} records", device.getName(), responseObj.records.size());
return responseObj.records;
}
private PortalAuthRequestParameter createAuthData() {
PortalLoginResponse loginData = this.loginData;
if (loginData == null) {
throw new IllegalStateException("Not logged in");
}
return new PortalAuthRequestParameter(configuration.getPortalAUthRequestWith(), loginData.getUserId(),
configuration.getRealm(), loginData.getToken(), configuration.getResource());
}
private <T> T handleResponseWrapper(@Nullable ResponseWrapper<T> response) throws EcovacsApiException {
if (response == null) {
// should not happen in practice
throw new EcovacsApiException("No response received");
}
if (!response.isSuccess()) {
throw new EcovacsApiException("API call failed: " + response.getMessage() + ", code " + response.getCode());
}
return response.getData();
}
private <T> T handleResponse(ContentResponse response, Class<T> clazz) throws EcovacsApiException {
@Nullable
T respObject = gson.fromJson(response.getContentAsString(), clazz);
if (respObject == null) {
// should not happen in practice
throw new EcovacsApiException("No response received");
}
return respObject;
}
private Request createAuthRequest(String url, String clientKey, String clientSecret,
Map<String, String> requestSpecificParameters) {
HashMap<String, String> signedRequestParameters = new HashMap<>(requestSpecificParameters);
signedRequestParameters.put("authTimespan", String.valueOf(System.currentTimeMillis()));
StringBuilder signOnText = new StringBuilder(clientKey);
signedRequestParameters.keySet().stream().sorted().forEach(key -> {
signOnText.append(key).append("=").append(signedRequestParameters.get(key));
});
signOnText.append(clientSecret);
signedRequestParameters.put("authAppkey", clientKey);
signedRequestParameters.put("authSign", MD5Util.getMD5Hash(signOnText.toString()));
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
signedRequestParameters.forEach(request::param);
return request;
}
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)));
}
private ContentResponse executeRequest(Request request) throws EcovacsApiException, InterruptedException {
request.timeout(10, TimeUnit.SECONDS);
try {
ContentResponse response = request.send();
if (response.getStatus() != HttpStatus.OK_200) {
throw new EcovacsApiException(response);
}
return response;
} catch (TimeoutException | ExecutionException e) {
throw new EcovacsApiException(e);
}
}
}

View File

@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2023 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
@NonNullByDefault
public final class EcovacsApiUrlFactory {
private EcovacsApiUrlFactory() {
// Prevent instantiation
}
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";
public static String getLoginUrl(EcovacsApiConfiguration config) {
return getMainUrl(config) + MAIN_URL_LOGIN_PATH;
}
public static String getAuthUrl(EcovacsApiConfiguration config) {
return String.format("https://gl-%1$s-openapi.ecovacs.%2$s/v1/global/auth/getAuthCode", config.getCountry(),
getApiUrlTld(config));
}
public static String getPortalUsersUrl(EcovacsApiConfiguration config) {
return getPortalUrl(config) + PORTAL_USERS_PATH;
}
public static String getPortalProductIotMapUrl(EcovacsApiConfiguration config) {
return getPortalUrl(config) + PORTAL_IOT_PRODUCT_PATH;
}
public static String getPortalIotDeviceManagerUrl(EcovacsApiConfiguration config) {
return getPortalUrl(config) + PORTAL_IOT_DEVMANAGER_PATH;
}
public static String getPortalLogUrl(EcovacsApiConfiguration config) {
return getPortalUrl(config) + PORTAL_LOG_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);
}
private static String getMainUrl(EcovacsApiConfiguration config) {
return String.format("https://gl-%1$s-api.ecovacs.%2$s/v1/private/%1$s/%3$s/%4$s/%5$s/%6$s/%7$s/%8$s",
config.getCountry(), getApiUrlTld(config), config.getLanguage(), config.getDeviceId(),
config.getAppCode(), config.getAppVersion(), config.getChannel(), config.getDeviceType());
}
private static String getApiUrlTld(EcovacsApiConfiguration config) {
return "cn".equalsIgnoreCase(config.getCountry()) ? "cn" : "com";
}
}

View File

@ -0,0 +1,211 @@
/**
* Copyright (c) 2010-2023 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;
import java.security.KeyStore;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration;
import org.openhab.binding.ecovacs.internal.api.EcovacsApiException;
import org.openhab.binding.ecovacs.internal.api.EcovacsDevice;
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.PortalLoginResponse;
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.util.DataParsingException;
import org.openhab.core.io.net.http.TrustAllTrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.hivemq.client.mqtt.MqttClient;
import com.hivemq.client.mqtt.MqttClientSslConfig;
import com.hivemq.client.mqtt.lifecycle.MqttClientDisconnectedListener;
import com.hivemq.client.mqtt.lifecycle.MqttDisconnectSource;
import com.hivemq.client.mqtt.mqtt3.Mqtt3AsyncClient;
import com.hivemq.client.mqtt.mqtt3.exceptions.Mqtt3ConnAckException;
import com.hivemq.client.mqtt.mqtt3.exceptions.Mqtt3DisconnectException;
import com.hivemq.client.mqtt.mqtt3.message.auth.Mqtt3SimpleAuth;
import com.hivemq.client.mqtt.mqtt3.message.connect.connack.Mqtt3ConnAckReturnCode;
import com.hivemq.client.mqtt.mqtt3.message.publish.Mqtt3Publish;
import io.netty.handler.ssl.util.SimpleTrustManagerFactory;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class EcovacsIotMqDevice implements EcovacsDevice {
private final Logger logger = LoggerFactory.getLogger(EcovacsIotMqDevice.class);
private final Device device;
private final DeviceDescription desc;
private final EcovacsApiImpl api;
private final Gson gson;
private @Nullable Mqtt3AsyncClient mqttClient;
EcovacsIotMqDevice(Device device, DeviceDescription desc, EcovacsApiImpl api, Gson gson)
throws EcovacsApiException {
this.device = device;
this.desc = desc;
this.api = api;
this.gson = gson;
}
@Override
public String getSerialNumber() {
return device.getName();
}
@Override
public String getModelName() {
return desc.modelName;
}
@Override
public boolean hasCapability(DeviceCapability cap) {
return desc.capabilities.contains(cap);
}
@Override
public <T> T sendCommand(IotDeviceCommand<T> command) throws EcovacsApiException, InterruptedException {
return api.sendIotCommand(device, desc, command);
}
@Override
public List<CleanLogRecord> getCleanLogs() throws EcovacsApiException, InterruptedException {
Stream<CleanLogRecord> logEntries;
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));
}
return logEntries.sorted((lhs, rhs) -> rhs.timestamp.compareTo(lhs.timestamp)).collect(Collectors.toList());
}
@Override
public void connect(final EventListener listener, ScheduledExecutorService scheduler)
throws EcovacsApiException, InterruptedException {
EcovacsApiConfiguration config = api.getConfig();
PortalLoginResponse loginData = api.getLoginData();
if (loginData == null) {
throw new EcovacsApiException("Can not connect when not logged in");
}
// XML message handler does not receive firmware version information with events, so fetch in advance
if (desc.protoVersion == ProtocolVersion.XML) {
listener.onFirmwareVersionChanged(this, sendCommand(new GetFirmwareVersionCommand()));
}
String userName = String.format("%s@%s", loginData.getUserId(), config.getRealm().split("\\.")[0]);
String host = String.format("mq-%s.%s", config.getContinent(), config.getRealm());
Mqtt3SimpleAuth auth = Mqtt3SimpleAuth.builder().username(userName).password(loginData.getToken().getBytes())
.build();
MqttClientSslConfig sslConfig = MqttClientSslConfig.builder().trustManagerFactory(createTrustManagerFactory())
.build();
final MqttClientDisconnectedListener disconnectListener = ctx -> {
boolean expectedShutdown = ctx.getSource() == MqttDisconnectSource.USER
&& ctx.getCause() instanceof Mqtt3DisconnectException;
// As the client already was disconnected, there's no need to do it again in disconnect() later
this.mqttClient = null;
if (!expectedShutdown) {
logger.debug("{}: MQTT disconnected (source {}): {}", getSerialNumber(), ctx.getSource(),
ctx.getCause().getMessage());
listener.onEventStreamFailure(EcovacsIotMqDevice.this, ctx.getCause());
}
};
final Mqtt3AsyncClient client = MqttClient.builder().useMqttVersion3()
.identifier(userName + "/" + loginData.getResource()).simpleAuth(auth).serverHost(host).serverPort(8883)
.sslConfig(sslConfig).addDisconnectedListener(disconnectListener).buildAsync();
try {
this.mqttClient = client;
client.connect().get();
final ReportParser parser = desc.protoVersion == ProtocolVersion.XML
? new XmlReportParser(this, listener, gson, logger)
: new JsonReportParser(this, listener, desc.protoVersion, gson, logger);
final Consumer<@Nullable Mqtt3Publish> eventCallback = publish -> {
if (publish == null) {
return;
}
String receivedTopic = publish.getTopic().toString();
String payload = new String(publish.getPayloadAsBytes());
try {
String eventName = receivedTopic.split("/")[2].toLowerCase();
logger.trace("{}: Got MQTT message on topic {}: {}", getSerialNumber(), receivedTopic, payload);
parser.handleMessage(eventName, payload);
} catch (DataParsingException e) {
listener.onEventStreamFailure(this, e);
}
};
String topic = String.format("iot/atr/+/%s/%s/%s/+", device.getDid(), device.getDeviceClass(),
device.getResource());
client.subscribeWith().topicFilter(topic).callback(eventCallback).send().get();
logger.debug("Established MQTT connection to device {}", getSerialNumber());
} catch (ExecutionException e) {
Throwable cause = e.getCause();
boolean isAuthFailure = cause instanceof Mqtt3ConnAckException && ((Mqtt3ConnAckException) cause)
.getMqttMessage().getReturnCode() == Mqtt3ConnAckReturnCode.NOT_AUTHORIZED;
throw new EcovacsApiException(e, isAuthFailure);
}
}
@Override
public void disconnect(ScheduledExecutorService scheduler) {
Mqtt3AsyncClient client = this.mqttClient;
if (client != null) {
client.disconnect();
}
this.mqttClient = null;
}
private TrustManagerFactory createTrustManagerFactory() {
return new SimpleTrustManagerFactory() {
@Override
protected void engineInit(@Nullable KeyStore keyStore) throws Exception {
}
@Override
protected void engineInit(@Nullable ManagerFactoryParameters managerFactoryParameters) throws Exception {
}
@Override
protected TrustManager[] engineGetTrustManagers() {
return new TrustManager[] { TrustAllTrustManager.getInstance() };
}
};
}
}

View File

@ -0,0 +1,467 @@
/**
* Copyright (c) 2010-2023 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;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
import org.jivesoftware.smack.packet.ErrorIQ;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smack.provider.IQProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.sasl.SASLErrorException;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smackx.ping.PingManager;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration;
import org.openhab.binding.ecovacs.internal.api.EcovacsApiException;
import org.openhab.binding.ecovacs.internal.api.EcovacsDevice;
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.PortalIotCommandXmlResponse;
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;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.openhab.binding.ecovacs.internal.api.util.SchedulerTask;
import org.openhab.binding.ecovacs.internal.api.util.XPathUtils;
import org.openhab.core.io.net.http.TrustAllTrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class EcovacsXmppDevice implements EcovacsDevice {
private final Logger logger = LoggerFactory.getLogger(EcovacsXmppDevice.class);
private final Device device;
private final DeviceDescription desc;
private final EcovacsApiImpl api;
private final Gson gson;
private @Nullable IncomingMessageHandler messageHandler;
private @Nullable PingHandler pingHandler;
private @Nullable XMPPTCPConnection connection;
private @Nullable Jid ownAddress;
private @Nullable Jid targetAddress;
EcovacsXmppDevice(Device device, DeviceDescription desc, EcovacsApiImpl api, Gson gson) {
this.device = device;
this.desc = desc;
this.api = api;
this.gson = gson;
}
@Override
public String getSerialNumber() {
return device.getName();
}
@Override
public String getModelName() {
return desc.modelName;
}
@Override
public boolean hasCapability(DeviceCapability cap) {
return desc.capabilities.contains(cap);
}
@Override
public <T> T sendCommand(IotDeviceCommand<T> command) throws EcovacsApiException, InterruptedException {
IncomingMessageHandler handler = this.messageHandler;
XMPPConnection conn = this.connection;
Jid from = this.ownAddress;
Jid to = this.targetAddress;
if (handler == null || conn == null || from == null || to == null) {
throw new EcovacsApiException("Not connected to device");
}
try {
// Devices sometimes send no answer to commands for unknown reasons. Ecovacs'
// app employs a similar retry mechanism, so this seems to be 'normal'.
for (int retry = 0; retry < 3; retry++) {
DeviceCommandIQ request = new DeviceCommandIQ(command, from, to);
CommandResponseHolder responseHolder = new CommandResponseHolder();
try {
handler.registerPendingCommand(request.id, responseHolder);
logger.trace("{}: sending command {}, retry {}", getSerialNumber(),
command.getName(ProtocolVersion.XML), retry);
synchronized (responseHolder) {
conn.sendIqRequestAsync(request);
responseHolder.wait(1500);
}
} finally {
handler.unregisterPendingCommand(request.id);
}
String response = responseHolder.response;
if (response != null) {
logger.trace("{}: Received command response XML {}", getSerialNumber(), response);
PortalIotCommandXmlResponse responseObj = new PortalIotCommandXmlResponse("", response, 0, "");
return command.convertResponse(responseObj, ProtocolVersion.XML, gson);
}
}
} catch (DataParsingException | ParserConfigurationException | TransformerException e) {
throw new EcovacsApiException(e);
}
throw new EcovacsApiException("No response for command " + command.getName(ProtocolVersion.XML));
}
@Override
public List<CleanLogRecord> getCleanLogs() throws EcovacsApiException, InterruptedException {
return sendCommand(new GetCleanLogsCommand());
}
@Override
public void connect(final EventListener listener, final ScheduledExecutorService scheduler)
throws EcovacsApiException {
EcovacsApiConfiguration config = api.getConfig();
PortalLoginResponse loginData = api.getLoginData();
if (loginData == null) {
throw new EcovacsApiException("Can not connect when not logged in");
}
logger.trace("{}: Connecting to XMPP", getSerialNumber());
String password = String.format("0/%s/%s", loginData.getResource(), loginData.getToken());
String host = String.format("msg-%s.%s", config.getContinent(), config.getRealm());
try {
Jid ownAddress = JidCreate.from(loginData.getUserId(), config.getRealm(), loginData.getResource());
Jid targetAddress = JidCreate.from(device.getDid(), device.getDeviceClass() + ".ecorobot.net", "atom");
XMPPTCPConnectionConfiguration connConfig = XMPPTCPConnectionConfiguration.builder().setHost(host)
.setPort(5223).setUsernameAndPassword(loginData.getUserId(), password)
.setResource(loginData.getResource()).setXmppDomain(config.getRealm())
.setCustomX509TrustManager(TrustAllTrustManager.getInstance()).setSendPresence(false).build();
XMPPTCPConnection conn = new XMPPTCPConnection(connConfig);
conn.addConnectionListener(new ConnectionListener() {
@Override
public void connected(@Nullable XMPPConnection connection) {
}
@Override
public void authenticated(@Nullable XMPPConnection connection, boolean resumed) {
}
@Override
public void connectionClosed() {
}
@Override
public void connectionClosedOnError(@Nullable Exception e) {
logger.trace("{}: XMPP connection failed", getSerialNumber(), e);
if (e != null) {
listener.onEventStreamFailure(EcovacsXmppDevice.this, e);
}
}
});
PingHandler pingHandler = new PingHandler(conn, scheduler, listener, targetAddress);
messageHandler = new IncomingMessageHandler(listener);
Roster roster = Roster.getInstanceFor(conn);
roster.setRosterLoadedAtLogin(false);
conn.registerIQRequestHandler(messageHandler);
conn.connect();
this.connection = conn;
this.ownAddress = ownAddress;
this.targetAddress = targetAddress;
this.pingHandler = pingHandler;
conn.login();
conn.setReplyTimeout(1000);
logger.trace("{}: XMPP connection established", getSerialNumber());
listener.onFirmwareVersionChanged(this, sendCommand(new GetFirmwareVersionCommand()));
pingHandler.start();
} catch (SASLErrorException e) {
throw new EcovacsApiException(e, true);
} catch (XMPPException | SmackException | InterruptedException | IOException e) {
throw new EcovacsApiException(e);
}
}
@Override
public void disconnect(ScheduledExecutorService scheduler) {
PingHandler pingHandler = this.pingHandler;
if (pingHandler != null) {
pingHandler.stop();
}
this.pingHandler = null;
IncomingMessageHandler handler = this.messageHandler;
if (handler != null) {
handler.dispose();
}
this.messageHandler = null;
final XMPPTCPConnection conn = this.connection;
if (conn != null) {
scheduler.execute(() -> conn.disconnect());
}
this.connection = null;
}
private class PingHandler {
private static final long INTERVAL_SECONDS = 30;
// After a failure, use shorter intervals since subsequent further failure is likely
private static final long POST_FAILURE_INTERVAL_SECONDS = 5;
private static final int MAX_FAILURES = 4;
private final XMPPTCPConnection connection;
private final PingManager pingManager;
private final EventListener listener;
private final Jid toAddress;
private final SchedulerTask pingTask;
private boolean started = false;
private int failedPings = 0;
PingHandler(XMPPTCPConnection connection, ScheduledExecutorService scheduler, EventListener listener, Jid to) {
this.connection = connection;
this.pingManager = PingManager.getInstanceFor(connection);
this.pingTask = new SchedulerTask(scheduler, logger, "Ping", this::sendPing);
this.listener = listener;
this.toAddress = to;
this.pingTask.setNamePrefix(getSerialNumber());
}
public void start() {
started = true;
scheduleNextPing(0);
}
public void stop() {
started = false;
pingTask.cancel();
}
private void sendPing() {
long timeSinceLastStanza = (System.currentTimeMillis() - connection.getLastStanzaReceived()) / 1000;
if (timeSinceLastStanza < currentPingInterval()) {
scheduleNextPing(timeSinceLastStanza);
return;
}
try {
if (pingManager.ping(this.toAddress)) {
logger.trace("{}: Pinged device", getSerialNumber());
failedPings = 0;
}
} catch (InterruptedException e) {
// only happens when we're stopped
} catch (SmackException e) {
++failedPings;
logger.debug("{}: Ping failed (#{}): {})", getSerialNumber(), failedPings, e.getMessage());
if (failedPings >= MAX_FAILURES) {
listener.onEventStreamFailure(EcovacsXmppDevice.this, e);
}
}
scheduleNextPing(0);
}
private synchronized void scheduleNextPing(long delta) {
pingTask.cancel();
if (started) {
pingTask.schedule(currentPingInterval() - delta);
}
}
private long currentPingInterval() {
return failedPings > 0 ? POST_FAILURE_INTERVAL_SECONDS : INTERVAL_SECONDS;
}
}
private class IncomingMessageHandler extends AbstractIqRequestHandler {
private final EventListener listener;
private final ReportParser parser;
private final ConcurrentHashMap<String, CommandResponseHolder> pendingCommands = new ConcurrentHashMap<>();
private boolean disposed;
IncomingMessageHandler(EventListener listener) {
super("query", "com:ctl", Type.set, Mode.async);
this.listener = listener;
this.parser = new XmlReportParser(EcovacsXmppDevice.this, listener, gson, logger);
}
void registerPendingCommand(String id, CommandResponseHolder responseHolder) {
pendingCommands.put(id, responseHolder);
}
void unregisterPendingCommand(String id) {
pendingCommands.remove(id);
}
void dispose() {
disposed = true;
}
@Override
public @Nullable IQ handleIQRequest(@Nullable IQ iqRequest) {
if (disposed) {
return null;
}
if (iqRequest instanceof DeviceCommandIQ) {
DeviceCommandIQ iq = (DeviceCommandIQ) iqRequest;
try {
if (!iq.id.isEmpty()) {
CommandResponseHolder responseHolder = pendingCommands.remove(iq.id);
if (responseHolder != null) {
synchronized (responseHolder) {
responseHolder.response = iq.payload;
responseHolder.notifyAll();
}
}
} else {
Optional<String> eventNameOpt = XPathUtils.getFirstXPathMatchOpt(iq.payload, "//ctl/@td")
.map(n -> n.getNodeValue());
if (eventNameOpt.isPresent()) {
logger.trace("{}: Received event message XML {}", getSerialNumber(), iq.payload);
parser.handleMessage(eventNameOpt.get(), iq.payload);
} else {
logger.debug("{}: Got unexpected XML payload {}", getSerialNumber(), iq.payload);
}
}
} catch (DataParsingException e) {
listener.onEventStreamFailure(EcovacsXmppDevice.this, e);
}
} else if (iqRequest instanceof ErrorIQ) {
StanzaError error = ((ErrorIQ) iqRequest).getError();
logger.trace("{}: Got error response {}", getSerialNumber(), error);
listener.onEventStreamFailure(EcovacsXmppDevice.this,
new XMPPException.XMPPErrorException(iqRequest, error));
}
return null;
}
}
private static class CommandResponseHolder {
@Nullable
String response;
}
private static class DeviceCommandIQ extends IQ {
static final String TAG_NAME = "query";
static final String NAMESPACE = "com:ctl";
private final String payload;
final String id;
// request
public DeviceCommandIQ(IotDeviceCommand<?> cmd, Jid from, Jid to)
throws ParserConfigurationException, TransformerException {
super(TAG_NAME, NAMESPACE);
setType(Type.set);
setTo(to);
setFrom(from);
this.id = createRequestId();
this.payload = cmd.getXmlPayload(id);
}
// response
public DeviceCommandIQ(@Nullable String id, String payload) {
super(TAG_NAME, NAMESPACE);
this.id = id != null ? id : "";
this.payload = payload.replaceAll("\n|\r", "");
}
@Override
protected @Nullable IQChildElementXmlStringBuilder getIQChildElementBuilder(
@Nullable IQChildElementXmlStringBuilder xml) {
if (xml != null) {
xml.rightAngleBracket();
xml.append(payload);
}
return xml;
}
private String createRequestId() {
// Ecovacs' app uses numbers for request IDs, so better constrain ourselves to that as well
int random8DigitNumber = 10000000 + new Random().nextInt(90000000);
return Integer.toString(random8DigitNumber);
}
}
private static class CommandIQProvider extends IQProvider<@Nullable DeviceCommandIQ> {
@Override
public @Nullable DeviceCommandIQ parse(@Nullable XmlPullParser parser, int initialDepth) throws Exception {
@Nullable
DeviceCommandIQ packet = null;
if (parser == null) {
return null;
}
outerloop: while (true) {
switch (parser.next()) {
case XmlPullParser.START_TAG:
if (parser.getDepth() == initialDepth + 1) {
String id = parser.getAttributeValue("", "id");
String payload = PacketParserUtils.parseElement(parser).toString();
packet = new DeviceCommandIQ(id, payload);
}
break;
case XmlPullParser.END_TAG:
if (parser.getDepth() == initialDepth) {
break outerloop;
}
break;
}
}
return packet;
}
}
static {
ProviderManager.addIQProvider(DeviceCommandIQ.TAG_NAME, DeviceCommandIQ.NAMESPACE, new CommandIQProvider());
}
}

View File

@ -0,0 +1,149 @@
/**
* Copyright (c) 2010-2023 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;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.EcovacsDevice;
import org.openhab.binding.ecovacs.internal.api.EcovacsDevice.EventListener;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.BatteryReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.ChargeReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.CleanReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.CleanReportV2;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.ErrorReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.StatsReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.WaterInfoReport;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandJsonResponse.JsonResponsePayloadWrapper;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.slf4j.Logger;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
class JsonReportParser implements ReportParser {
private final EcovacsDevice device;
private final EventListener listener;
private final Gson gson;
private final Logger logger;
private String lastFirmwareVersion = "";
JsonReportParser(EcovacsDevice device, EventListener listener, ProtocolVersion version, Gson gson, Logger logger) {
this.device = device;
this.listener = listener;
this.gson = gson;
this.logger = logger;
}
@Override
public void handleMessage(String eventName, String payload) throws DataParsingException {
JsonResponsePayloadWrapper response;
try {
response = gson.fromJson(payload, JsonResponsePayloadWrapper.class);
} catch (JsonSyntaxException e) {
// The onFwBuryPoint-bd_sysinfo sends a JSON array instead of the expected JsonResponsePayloadBody object.
// Since we don't do anything with it anyway, just ignore it
logger.debug("{}: Got invalid JSON message payload, ignoring: {}", device.getSerialNumber(), payload, e);
response = null;
}
if (response == null) {
return;
}
if (!lastFirmwareVersion.equals(response.header.firmwareVersion)) {
lastFirmwareVersion = response.header.firmwareVersion;
listener.onFirmwareVersionChanged(device, lastFirmwareVersion);
}
if (eventName.startsWith("on")) {
eventName = eventName.substring(2);
} else if (eventName.startsWith("report")) {
eventName = eventName.substring(6);
}
switch (eventName) {
case "battery": {
BatteryReport report = payloadAs(response, BatteryReport.class);
listener.onBatteryLevelUpdated(device, report.percent);
break;
}
case "chargestate": {
ChargeReport report = payloadAs(response, ChargeReport.class);
listener.onChargingStateUpdated(device, report.isCharging != 0);
break;
}
case "cleaninfo": {
CleanReport report = payloadAs(response, CleanReport.class);
CleanMode mode = report.determineCleanMode(gson);
if (mode == null) {
throw new DataParsingException("Could not get clean mode from response " + payload);
}
String area = report.cleanState != null ? report.cleanState.areaDefinition : null;
handleCleanModeChange(mode, area);
break;
}
case "cleaninfo_v2": {
CleanReportV2 report = payloadAs(response, CleanReportV2.class);
CleanMode mode = report.determineCleanMode(gson);
if (mode == null) {
throw new DataParsingException("Could not get clean mode from response " + payload);
}
String area = report.cleanState != null && report.cleanState.content != null
? report.cleanState.content.areaDefinition
: null;
handleCleanModeChange(mode, area);
break;
}
case "error": {
ErrorReport report = payloadAs(response, ErrorReport.class);
for (Integer code : report.errorCodes) {
listener.onErrorReported(device, code);
}
}
case "stats": {
StatsReport report = payloadAs(response, StatsReport.class);
listener.onCleaningStatsUpdated(device, report.area, report.timeInSeconds);
break;
}
case "waterinfo": {
WaterInfoReport report = payloadAs(response, WaterInfoReport.class);
listener.onWaterSystemPresentUpdated(device, report.waterPlatePresent != 0);
break;
}
// more possible events (unused for now):
// - "evt" -> EventReport
// - "lifespan" -> ComponentLifeSpanReport
// - "speed" -> SpeedReport
}
}
private void handleCleanModeChange(CleanMode mode, @Nullable String areaDefinition) {
if (mode == CleanMode.CUSTOM_AREA) {
logger.debug("{}: Custom area cleaning stated with area definition {}", device.getSerialNumber(),
areaDefinition);
}
listener.onCleaningModeUpdated(device, mode, Optional.ofNullable(areaDefinition));
}
private <T> T payloadAs(JsonResponsePayloadWrapper response, Class<T> clazz) throws DataParsingException {
@Nullable
T payload = gson.fromJson(response.body.payload, clazz);
if (payload == null) {
throw new DataParsingException("Null payload in response " + response);
}
return payload;
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2023 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
@NonNullByDefault
public enum PortalTodo {
@SerializedName("GetDeviceList")
GET_DEVICE_LIST,
@SerializedName("loginByItToken")
LOGIN_BY_TOKEN;
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2023 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public enum ProtocolVersion {
@SerializedName("xml")
XML,
@SerializedName("json")
JSON,
@SerializedName("json_v2")
JSON_V2
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2023 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public interface ReportParser {
void handleMessage(String eventName, String payload) throws DataParsingException;
}

View File

@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2023 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;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.EcovacsDevice;
import org.openhab.binding.ecovacs.internal.api.EcovacsDevice.EventListener;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.CleaningInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.DeviceInfo;
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.xml.WaterSystemInfo;
import org.openhab.binding.ecovacs.internal.api.model.ChargeMode;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.openhab.binding.ecovacs.internal.api.util.XPathUtils;
import org.slf4j.Logger;
import org.w3c.dom.Node;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
class XmlReportParser implements ReportParser {
private final EcovacsDevice device;
private final EventListener listener;
private final Gson gson;
private final Logger logger;
XmlReportParser(EcovacsDevice device, EventListener listener, Gson gson, Logger logger) {
this.device = device;
this.listener = listener;
this.gson = gson;
this.logger = logger;
}
@Override
public void handleMessage(String eventName, String payload) throws DataParsingException {
switch (eventName.toLowerCase()) {
case "batteryinfo":
listener.onBatteryLevelUpdated(device, DeviceInfo.parseBatteryInfo(payload));
break;
case "chargestate": {
ChargeMode mode = DeviceInfo.parseChargeInfo(payload, gson);
if (mode == ChargeMode.RETURNING) {
listener.onCleaningModeUpdated(device, CleanMode.RETURNING, Optional.empty());
}
listener.onChargingStateUpdated(device, mode == ChargeMode.CHARGING);
break;
}
case "cleanreport": {
CleaningInfo.CleanStateInfo info = CleaningInfo.parseCleanStateInfo(payload, gson);
if (info.mode == CleanMode.CUSTOM_AREA) {
logger.debug("{}: Custom area cleaning stated with area definition {}", device.getSerialNumber(),
info.areaDefinition);
}
listener.onCleaningModeUpdated(device, info.mode, info.areaDefinition);
// Full report:
// <ctl td='CleanReport'><clean type='auto' speed='standard' st='s' rsn='a'/></ctl>
break;
}
case "cleanrptbgdata": {
Node fromChargerNode = XPathUtils.getFirstXPathMatch(payload, "//@IsFrmCharger");
if ("1".equals(fromChargerNode.getNodeValue())) {
// Device just started cleaning, but likely won't send us a ChargeState report,
// so update charging state from here
listener.onChargingStateUpdated(device, false);
}
// Full report:
// <ctl td='CleanRptBgdata' ts='1643044172' Battery='102' CleanID='1333688018' iCleanID='0497265223'
// MapID='1430814334' rsn='a' IsFrmCharger='1' CleanType='auto' Speed='standard' OnOffRag='0'
// WorkMode='s' Spray='2' WorkArea='002'/>
break;
}
case "cleanst": {
String area = XPathUtils.getFirstXPathMatch(payload, "//@a").getNodeValue();
String duration = XPathUtils.getFirstXPathMatch(payload, "//@l").getNodeValue();
listener.onCleaningStatsUpdated(device, Integer.valueOf(area), Integer.valueOf(duration));
break;
}
case "error":
DeviceInfo.parseErrorInfo(payload).ifPresent(errorCode -> {
listener.onErrorReported(device, errorCode);
});
break;
case "waterboxinfo":
listener.onWaterSystemPresentUpdated(device, WaterSystemInfo.parseWaterBoxInfo(payload));
break;
}
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2023 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.request.portal;
import org.openhab.binding.ecovacs.internal.api.impl.PortalTodo;
import com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
public class PortalAuthRequest {
@SerializedName("todo")
final PortalTodo todo;
@SerializedName("userid")
final String userId;
@SerializedName("auth")
final PortalAuthRequestParameter auth;
public PortalAuthRequest(PortalTodo todo, PortalAuthRequestParameter auth) {
this.todo = todo;
this.userId = auth.userId;
this.auth = auth;
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2023 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.request.portal;
import com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
public class PortalAuthRequestParameter {
@SerializedName("with")
final String with;
@SerializedName("userid")
final String userId;
@SerializedName("realm")
final String realm;
@SerializedName("token")
final String token;
@SerializedName("resource")
final String resource;
public PortalAuthRequestParameter(String with, String userid, String realm, String token, String resource) {
this.with = with;
this.userId = userid;
this.realm = realm;
this.token = token;
this.resource = resource;
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2023 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.request.portal;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class PortalCleanLogsRequest {
@SerializedName("auth")
final PortalAuthRequestParameter auth;
@SerializedName("td")
final String commandName = "GetCleanLogs";
@SerializedName("did")
final String targetDeviceId;
@SerializedName("resource")
final String targetResource;
public PortalCleanLogsRequest(PortalAuthRequestParameter auth, String targetDeviceId, String targetResource) {
this.auth = auth;
this.targetDeviceId = targetDeviceId;
this.targetResource = targetResource;
}
}

View File

@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2023 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.request.portal;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class PortalIotCommandRequest {
@SerializedName("auth")
final PortalAuthRequestParameter auth;
@SerializedName("cmdName")
final String commandName;
@SerializedName("payload")
final Object payload;
@SerializedName("payloadType")
final String payloadType;
@SerializedName("td")
final String td = "q";
@SerializedName("toId")
final String targetDeviceId;
@SerializedName("toRes")
final String targetResource;
@SerializedName("toType")
final String targetClass;
public PortalIotCommandRequest(PortalAuthRequestParameter auth, String commandName, Object payload,
String targetDeviceId, String targetResource, String targetClass, boolean json) {
this.auth = auth;
this.commandName = commandName;
this.payload = payload;
this.targetDeviceId = targetDeviceId;
this.targetResource = targetResource;
this.targetClass = targetClass;
this.payloadType = json ? "j" : "x";
}
public static class JsonPayloadHeader {
@SerializedName("pri")
public final int pri = 1;
@SerializedName("ts")
public final long timestamp;
@SerializedName("tzm")
public final int tzm = 480;
@SerializedName("ver")
public final String version = "0.0.50";
public JsonPayloadHeader() {
timestamp = System.currentTimeMillis();
}
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2023 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.request.portal;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class PortalIotProductRequest {
@SerializedName("todo")
final String todo = "";
@SerializedName("channel")
final String channel = "";
@SerializedName("auth")
final PortalAuthRequestParameter auth;
public PortalIotProductRequest(PortalAuthRequestParameter auth) {
this.auth = auth;
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2023 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.request.portal;
import org.openhab.binding.ecovacs.internal.api.impl.PortalTodo;
import com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
public class PortalLoginRequest {
@SerializedName("todo")
final PortalTodo todo;
@SerializedName("country")
final String country;
@SerializedName("last")
final String last;
@SerializedName("org")
final String org;
@SerializedName("resource")
final String resource;
@SerializedName("realm")
final String realm;
@SerializedName("token")
final String token;
@SerializedName("userid")
final String userId;
@SerializedName("edition")
final String edition;
public PortalLoginRequest(PortalTodo todo, String country, String last, String org, String resource, String realm,
String token, String userId, String edition) {
this.todo = todo;
this.country = country;
this.last = last;
this.org = org;
this.resource = resource;
this.realm = realm;
this.token = token;
this.userId = userId;
this.edition = edition;
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class BatteryReport {
@SerializedName("value")
public int percent;
@SerializedName("isLow")
public int batteryIsLow;
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class CachedMapInfoReport {
@SerializedName("enable")
public int enable;
@SerializedName("info")
public List<CachedMapInfo> mapInfos;
public static class CachedMapInfo {
@SerializedName("mid")
public String mapId;
public int index;
public int status;
@SerializedName("using")
public int used;
public int built;
public String name;
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class ChargeReport {
@SerializedName("isCharging")
public int isCharging;
@SerializedName("mode")
public String mode; // slot, ...?
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class CleanReport {
@SerializedName("trigger")
public String trigger; // app, workComplete, ...?
@SerializedName("state")
public String state;
@SerializedName("cleanState")
public CleanStateReport cleanState;
public static class CleanStateReport {
@SerializedName("router")
public String router; // plan, ...?
@SerializedName("type")
public String type;
@SerializedName("motionState")
public String motionState;
@SerializedName("content")
public String areaDefinition;
}
public CleanMode determineCleanMode(Gson gson) {
final String modeValue;
if (cleanState != null) {
if ("working".equals(cleanState.motionState)) {
modeValue = cleanState.type;
} else {
modeValue = cleanState.motionState;
}
} else {
modeValue = state;
}
return gson.fromJson(modeValue, CleanMode.class);
}
}

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class CleanReportV2 {
@SerializedName("trigger")
public String trigger; // app, workComplete, ...?
@SerializedName("state")
public String state;
@SerializedName("cleanState")
public CleanStateReportV2 cleanState;
public static class CleanStateReportV2 {
@SerializedName("router")
public String router; // plan, ...?
@SerializedName("motionState")
public String motionState;
@SerializedName("content")
public CleanStateReportV2Content content;
}
public static class CleanStateReportV2Content {
@SerializedName("type")
public String type;
@SerializedName("value")
public String areaDefinition;
}
public CleanMode determineCleanMode(Gson gson) {
final String modeValue;
if ("clean".equals(state) && cleanState != null) {
if ("working".equals(cleanState.motionState)) {
modeValue = cleanState.content.type;
} else {
modeValue = cleanState.motionState;
}
} else {
modeValue = state;
}
return gson.fromJson(modeValue, CleanMode.class);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class ComponentLifeSpanReport {
@SerializedName("type")
public String type;
@SerializedName("left")
public int left;
@SerializedName("total")
public int total;
}

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
/**
* @author Danny Baumann - Initial contribution
*/
public class DefaultCleanCountReport {
public int count;
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class EnabledStateReport {
@SerializedName("enable")
public int enabled;
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class ErrorReport {
@SerializedName("code")
public List<Integer> errorCodes;
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class EventReport {
@SerializedName("code")
public int eventCode;
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class MapSetReport {
public String type;
public int count;
@SerializedName("mid")
public String mapId;
@SerializedName("msid")
public String mapSetId;
public List<MapSubSetInfo> subsets;
public static class MapSubSetInfo {
@SerializedName("mssid")
public String id;
}
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
/**
* @author Danny Baumann - Initial contribution
*/
public class NetworkInfoReport {
public String ip;
public String mac;
public String ssid;
public String rssi;
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class SleepReport {
@SerializedName("enable")
public int sleeping;
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class SpeedReport {
@SerializedName("speed")
public int speedLevel;
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class StatsReport {
@SerializedName("area")
public int area;
@SerializedName("time")
public int timeInSeconds;
@SerializedName("cid")
public String cid;
@SerializedName("start")
public long startTimestamp;
@SerializedName("type")
public String type; // auto, ... ?
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.json;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class WaterInfoReport {
@SerializedName("enable")
public int waterPlatePresent;
@SerializedName("amount")
public int waterAmount;
}

View File

@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.xml;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
import org.openhab.binding.ecovacs.internal.api.model.SuctionPower;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.openhab.binding.ecovacs.internal.api.util.XPathUtils;
import org.w3c.dom.Node;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class CleaningInfo {
public static class CleanStateInfo {
public final CleanMode mode;
public final Optional<String> areaDefinition;
CleanStateInfo(CleanMode mode) {
this(mode, Optional.empty());
}
CleanStateInfo(CleanMode mode, Optional<String> areaDefinition) {
this.mode = mode;
this.areaDefinition = areaDefinition;
}
}
public static CleanStateInfo parseCleanStateInfo(String xml, Gson gson) throws DataParsingException {
String stateString = XPathUtils.getFirstXPathMatchOpt(xml, "//clean/@st").map(n -> n.getNodeValue()).orElse("");
if ("h".equals(stateString)) {
return new CleanStateInfo(CleanMode.STOP);
} else if ("p".equals(stateString)) {
return new CleanStateInfo(CleanMode.PAUSE);
} else {
String modeString = XPathUtils.getFirstXPathMatch(xml, "//clean/@type").getNodeValue();
CleanMode parsedMode = gson.fromJson(modeString, CleanMode.class);
if (parsedMode == CleanMode.SPOT_AREA) {
Optional<Node> pointOpt = XPathUtils.getFirstXPathMatchOpt(xml, "//clean/@p");
if (pointOpt.isPresent()) {
return new CleanStateInfo(CleanMode.CUSTOM_AREA, pointOpt.map(n -> n.getNodeValue()));
}
Optional<Node> midOpt = XPathUtils.getFirstXPathMatchOpt(xml, "//clean/@mid");
return new CleanStateInfo(CleanMode.SPOT_AREA, midOpt.map(n -> n.getNodeValue()));
}
if (parsedMode != null) {
return new CleanStateInfo(parsedMode);
}
}
throw new DataParsingException("Unexpected clean state report: " + xml);
}
public static SuctionPower parseCleanSpeedInfo(String xml, Gson gson) throws DataParsingException {
String levelString = XPathUtils.getFirstXPathMatch(xml, "//@speed").getNodeValue();
SuctionPower level = gson.fromJson(levelString, SuctionPower.class);
if (level == null) {
throw new DataParsingException("Could not parse power level " + levelString);
}
return level;
}
}

View File

@ -0,0 +1,95 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.xml;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.model.ChargeMode;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.openhab.binding.ecovacs.internal.api.util.XPathUtils;
import org.w3c.dom.Node;
import com.google.gson.Gson;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class DeviceInfo {
private static final Set<String> ERROR_ATTR_NAMES = Set.of("code", "error", "errno", "errs");
public static int parseBatteryInfo(String xml) throws DataParsingException {
Node batteryAttr = XPathUtils.getFirstXPathMatch(xml, "//battery/@power");
return Integer.valueOf(batteryAttr.getNodeValue());
}
public static ChargeMode parseChargeInfo(String xml, Gson gson) throws DataParsingException {
String modeString = XPathUtils.getFirstXPathMatch(xml, "//charge/@type").getNodeValue();
ChargeMode mode = gson.fromJson(modeString, ChargeMode.class);
if (mode == null) {
throw new IllegalArgumentException("Could not parse charge mode " + modeString);
}
return mode;
}
public static Optional<Integer> parseErrorInfo(String xml) throws DataParsingException {
for (String attr : ERROR_ATTR_NAMES) {
Optional<Node> node = XPathUtils.getFirstXPathMatchOpt(xml, "//@" + attr);
if (node.isPresent()) {
try {
String value = node.get().getNodeValue();
return value.isEmpty() ? Optional.empty() : Optional.of(Integer.valueOf(value));
} catch (NumberFormatException e) {
throw new DataParsingException(e);
}
}
}
return Optional.empty();
}
public static int parseComponentLifespanInfo(String xml) throws DataParsingException {
Optional<Integer> value = nodeValueToInt(xml, "value");
Optional<Integer> total = nodeValueToInt(xml, "total");
Optional<Integer> left = nodeValueToInt(xml, "left");
if (value.isPresent() && total.isPresent()) {
return (int) Math.round(100.0 * value.get() / total.get());
} else if (value.isPresent()) {
return (int) Math.round(0.01 * value.get());
} else if (left.isPresent() && total.isPresent()) {
return (int) Math.round(100.0 * left.get() / total.get());
} else if (left.isPresent()) {
return (int) Math.round((double) left.get() / 60.0);
}
return 0;
}
public static boolean parseEnabledStateInfo(String xml) throws DataParsingException {
String value = XPathUtils.getFirstXPathMatch(xml, "//@on").getNodeValue();
try {
return Integer.valueOf(value) != 0;
} catch (NumberFormatException e) {
throw new DataParsingException(e);
}
}
private static Optional<Integer> nodeValueToInt(String xml, String attrName) throws DataParsingException {
try {
return XPathUtils.getFirstXPathMatchOpt(xml, "//ctl/@" + attrName)
.map(n -> Integer.valueOf(n.getNodeValue()));
} catch (NumberFormatException e) {
throw new DataParsingException(e);
}
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2023 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.deviceapi.xml;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ecovacs.internal.api.model.MoppingWaterAmount;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import org.openhab.binding.ecovacs.internal.api.util.XPathUtils;
import org.w3c.dom.Node;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class WaterSystemInfo {
/**
* @return Whether water system is present
*/
public static boolean parseWaterBoxInfo(String xml) throws DataParsingException {
Node node = XPathUtils.getFirstXPathMatch(xml, "//@on");
return Integer.valueOf(node.getNodeValue()) != 0;
}
public static MoppingWaterAmount parseWaterPermeabilityInfo(String xml) throws DataParsingException {
Node node = XPathUtils.getFirstXPathMatch(xml, "//@v");
try {
return MoppingWaterAmount.fromApiValue(Integer.valueOf(node.getNodeValue()));
} catch (NumberFormatException e) {
throw new DataParsingException(e);
}
}
}

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2023 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.main;
import com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
public class AccessData {
@SerializedName("uid")
private final String uid;
@SerializedName("accessToken")
private final String accessToken;
@SerializedName("userName")
private final String userName;
@SerializedName("email")
private final String email;
@SerializedName("mobile")
private final String mobile;
@SerializedName("isNew")
private final boolean isNew;
@SerializedName("loginName")
private final String loginName;
@SerializedName("ucUid")
private final String ucUid;
public AccessData(String uid, String accessToken, String userName, String email, String mobile, boolean isNew,
String loginName, String ucUid) {
this.uid = uid;
this.accessToken = accessToken;
this.userName = userName;
this.email = email;
this.mobile = mobile;
this.isNew = isNew;
this.loginName = loginName;
this.ucUid = ucUid;
}
public String getUid() {
return uid;
}
public String getAccessToken() {
return accessToken;
}
public String getUserName() {
return userName;
}
public String getEmail() {
return email;
}
public String getMobile() {
return mobile;
}
public boolean isNew() {
return isNew;
}
public String getLoginName() {
return loginName;
}
public String getUcUid() {
return ucUid;
}
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2023 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.main;
import com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
public class AuthCode {
@SerializedName("ecovacsUid")
private final String ecovacsUid;
@SerializedName("authCode")
private final String authCode;
public AuthCode(String ecovacsUid, String authCode) {
this.ecovacsUid = ecovacsUid;
this.authCode = authCode;
}
public String getEcovacsUid() {
return ecovacsUid;
}
public String getAuthCode() {
return authCode;
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2023 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.main;
import com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
public class ResponseWrapper<T> {
@SerializedName("code")
private final String code;
@SerializedName("time")
private final String time;
@SerializedName("msg")
private final String message;
@SerializedName("data")
private final T data;
@SerializedName("success")
private final boolean success;
public ResponseWrapper(String code, String time, String message, T data, boolean success) {
this.code = code;
this.time = time;
this.message = message;
this.data = data;
this.success = success;
}
public String getCode() {
return code;
}
public String getTime() {
return time;
}
public String getMessage() {
return message;
}
public T getData() {
return data;
}
public boolean isSuccess() {
return success;
}
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2023 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 com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class AbstractPortalIotCommandResponse {
@SerializedName("ret")
private final String result;
@SerializedName("errno")
private final int errorCode;
@SerializedName("error")
private final String errorMessage;
// unused field: 'id' (string)
public AbstractPortalIotCommandResponse(String result, int errorCode, String errorMessage) {
this.result = result;
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
public boolean wasSuccessful() {
return "ok".equals(result);
}
public boolean failedDueToAuthProblem() {
return "fail".equals(result) && errorMessage != null && errorMessage.toLowerCase().contains("auth error");
}
public String getErrorMessage() {
if (wasSuccessful()) {
return null;
}
return "result=" + result + ", errno=" + errorCode + ", error=" + errorMessage;
}
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2023 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 com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
public abstract class AbstractPortalResponse {
@SerializedName("result")
private final String result;
// unused field: 'todo' (string)
protected AbstractPortalResponse(String result) {
this.result = result;
}
public boolean wasSuccessful() {
return "ok".equals(result);
}
}

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2023 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 com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
public class Device {
@SerializedName("did")
private final String did;
@SerializedName("name")
private final String name;
@SerializedName("class")
private final String deviceClass;
@SerializedName("resource")
private final String resource;
@SerializedName("nick")
private final String nick;
@SerializedName("company")
private final String company;
@SerializedName("bindTs")
private final long bindTs;
@SerializedName("service")
private final Service service;
public Device(String did, String name, String deviceClass, String resource, String nick, String company,
long bindTs, Service service) {
this.did = did;
this.name = name;
this.deviceClass = deviceClass;
this.resource = resource;
this.nick = nick;
this.company = company;
this.bindTs = bindTs;
this.service = service;
}
public String getDid() {
return did;
}
public String getName() {
return name;
}
public String getDeviceClass() {
return deviceClass;
}
public String getResource() {
return resource;
}
public String getNick() {
return nick;
}
public String getCompany() {
return company;
}
public long getBindTs() {
return bindTs;
}
public Service getService() {
return service;
}
}

View File

@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2023 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 com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class IotProduct {
@SerializedName("classid")
private final String classId;
@SerializedName("product")
private final ProductDefinition productDef;
public IotProduct(String classId, ProductDefinition productDef) {
this.classId = classId;
this.productDef = productDef;
}
public String getClassId() {
return classId;
}
public ProductDefinition getDefinition() {
return productDef;
}
public static class ProductDefinition {
@SerializedName("_id")
public final String id;
@SerializedName("materialNo")
public final String materialNumber;
@SerializedName("name")
public final String name;
@SerializedName("icon")
public final String icon;
@SerializedName("iconUrl")
public final String iconUrl;
@SerializedName("model")
public final String model;
@SerializedName("UILogicId")
public final String uiLogicId;
@SerializedName("ota")
public final boolean otaCapable;
@SerializedName("supportType")
public final SupportFlags supportFlags;
public ProductDefinition(String id, String materialNumber, String name, String icon, String iconUrl,
String model, String uiLogicId, boolean otaCapable, SupportFlags supportFlags) {
this.id = id;
this.materialNumber = materialNumber;
this.name = name;
this.icon = icon;
this.iconUrl = iconUrl;
this.model = model;
this.uiLogicId = uiLogicId;
this.otaCapable = otaCapable;
this.supportFlags = supportFlags;
}
}
public static class SupportFlags {
@SerializedName("share")
public final boolean canShare;
@SerializedName("tmjl")
public final boolean tmjl; // ???
@SerializedName("assistant")
public final boolean canUseAssistant;
@SerializedName("alexa")
public final boolean canUseAlexa;
public SupportFlags(boolean share, boolean tmjl, boolean assistant, boolean alexa) {
this.canShare = share;
this.tmjl = tmjl;
this.canUseAssistant = assistant;
this.canUseAlexa = alexa;
}
}
}

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2023 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 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;
@SerializedName("ret")
final String result;
PortalCleanLogsResponse(String result, List<LogRecord> records) {
this.result = result;
this.records = records;
}
public boolean wasSuccessful() {
return "ok".equals(result);
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2023 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 Johannes Ptaszyk - Initial contribution
*/
public class PortalDeviceResponse extends AbstractPortalResponse {
@SerializedName("devices")
private final List<Device> devices;
public PortalDeviceResponse(String result, List<Device> devices) {
super(result);
this.devices = devices;
}
public List<Device> getDevices() {
return devices;
}
}

View File

@ -0,0 +1,90 @@
/**
* Copyright (c) 2010-2023 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.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class PortalIotCommandJsonResponse extends AbstractPortalIotCommandResponse {
@SerializedName("resp")
public final JsonElement response;
public PortalIotCommandJsonResponse(String result, JsonElement response, int errorCode, String errorMessage) {
super(result, errorCode, errorMessage);
this.response = response;
}
public <T> T getResponsePayloadAs(Gson gson, Class<T> clazz) throws DataParsingException {
try {
JsonElement payloadRaw = getResponsePayload(gson);
@Nullable
T payload = gson.fromJson(payloadRaw, clazz);
if (payload == null) {
throw new DataParsingException("Empty JSON payload");
}
return payload;
} catch (JsonSyntaxException e) {
throw new DataParsingException(e);
}
}
public JsonElement getResponsePayload(Gson gson) throws DataParsingException {
try {
@Nullable
JsonResponsePayloadWrapper wrapper = gson.fromJson(response, JsonResponsePayloadWrapper.class);
if (wrapper == null) {
throw new DataParsingException("Empty JSON payload");
}
return wrapper.body.payload;
} catch (JsonSyntaxException e) {
throw new DataParsingException(e);
}
}
public static class JsonPayloadHeader {
@SerializedName("pri")
public int pri;
@SerializedName("ts")
public long timestamp;
@SerializedName("tzm")
public int tzm;
@SerializedName("fwVer")
public String firmwareVersion;
@SerializedName("hwVer")
public String hardwareVersion;
}
public static class JsonResponsePayloadWrapper {
@SerializedName("header")
public JsonPayloadHeader header;
@SerializedName("body")
public JsonResponsePayloadBody body;
}
public static class JsonResponsePayloadBody {
@SerializedName("code")
public int code;
@SerializedName("msg")
public String message;
@SerializedName("data")
public JsonElement payload;
}
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2023 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 com.google.gson.annotations.SerializedName;
/**
* @author Danny Baumann - Initial contribution
*/
public class PortalIotCommandXmlResponse extends AbstractPortalIotCommandResponse {
@SerializedName("resp")
private final String responseXml;
public PortalIotCommandXmlResponse(String result, String responseXml, int errorCode, String errorMessage) {
super(result, errorCode, errorMessage);
this.responseXml = responseXml;
}
public String getResponsePayloadXml() {
return responseXml != null ? responseXml.replaceAll("\n|\r", "") : null;
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2023 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 PortalIotProductResponse {
@SerializedName("data")
private final List<IotProduct> products;
// unused field: 'code' (integer)
public PortalIotProductResponse(List<IotProduct> products) {
this.products = products;
}
public List<IotProduct> getProducts() {
return products;
}
}

Some files were not shown because too many files have changed in this diff Show More