[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
128 changed files with 9421 additions and 0 deletions

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

View File

@@ -0,0 +1,57 @@
/**
* 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 PortalLoginResponse extends AbstractPortalResponse {
@SerializedName("userId")
private final String userId;
@SerializedName("resource")
private final String resource;
@SerializedName("token")
private final String token;
@SerializedName("last")
private final String last;
public PortalLoginResponse(String result, String userId, String resource, String token, String last) {
super(result);
this.userId = userId;
this.resource = resource;
this.token = token;
this.last = last;
}
public String getUserId() {
return userId;
}
public String getResource() {
return resource;
}
public String getToken() {
return token;
}
public String getLast() {
return last;
}
}

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.portal;
import com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
public class Service {
@SerializedName("jmq")
private final String jmq;
@SerializedName("mqs")
private final String mqs;
public Service(String jmq, String mqs) {
this.jmq = jmq;
this.mqs = mqs;
}
public String getJmq() {
return jmq;
}
public String getMqs() {
return mqs;
}
}

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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
@NonNullByDefault
public enum ChargeMode {
@SerializedName("go")
RETURN,
@SerializedName("Going")
RETURNING,
@SerializedName("SlotCharging")
CHARGING,
@SerializedName("Idle")
IDLE;
}

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.model;
import java.util.Date;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Danny Baumann - Initial contribution
*/
@NonNullByDefault
public class CleanLogRecord {
public final Date timestamp;
public final long cleaningDuration;
public final int cleanedArea;
public final Optional<String> mapImageUrl;
public final CleanMode mode;
public CleanLogRecord(long timestamp, long duration, int area, Optional<String> mapImageUrl, CleanMode mode) {
this.timestamp = new Date(timestamp * 1000);
this.cleaningDuration = duration;
this.cleanedArea = area;
this.mapImageUrl = mapImageUrl;
this.mode = mode;
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* @author Johannes Ptaszyk - Initial contribution
*/
@NonNullByDefault
public enum CleanMode {
@SerializedName("auto")
AUTO,
@SerializedName("border")
EDGE,
@SerializedName("spot")
SPOT,
@SerializedName(value = "SpotArea", alternate = { "spotArea" })
SPOT_AREA,
@SerializedName(value = "CustomArea", alternate = { "customArea" })
CUSTOM_AREA,
@SerializedName("singleRoom")
SINGLE_ROOM,
@SerializedName("pause")
PAUSE,
@SerializedName("stop")
STOP,
@SerializedName(value = "going", alternate = { "goCharging" })
RETURNING,
@SerializedName("washing")
WASHING,
@SerializedName("drying")
DRYING,
@SerializedName("idle")
IDLE;
public boolean isActive() {
return this == AUTO || this == EDGE || this == SPOT || this == SPOT_AREA || this == CUSTOM_AREA
|| this == SINGLE_ROOM;
}
public boolean isIdle() {
return this == IDLE || this == DRYING || this == WASHING;
}
}

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