From efa16cd7ab0d6868fd791c5c1ffff3684589e810 Mon Sep 17 00:00:00 2001 From: maniac103 Date: Tue, 21 Mar 2023 11:05:53 +0100 Subject: [PATCH] [ecovacs] Initial contribution (#12231) * [ecovacs] Initial contribution Add initial version of a binding for vacuum cleaners made by Ecovacs. Signed-off-by: Danny Baumann --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.ecovacs/NOTICE | 13 + bundles/org.openhab.binding.ecovacs/README.md | 175 ++++ bundles/org.openhab.binding.ecovacs/pom.xml | 51 ++ .../src/main/feature/feature.xml | 22 + .../internal/EcovacsBindingConstants.java | 120 +++ ...covacsDynamicStateDescriptionProvider.java | 72 ++ .../internal/EcovacsHandlerFactory.java | 77 ++ .../internal/action/EcovacsVacuumActions.java | 84 ++ .../ecovacs/internal/api/EcovacsApi.java | 33 + .../internal/api/EcovacsApiConfiguration.java | 140 +++ .../internal/api/EcovacsApiException.java | 48 + .../ecovacs/internal/api/EcovacsDevice.java | 62 ++ .../commands/AbstractAreaCleaningCommand.java | 83 ++ .../api/commands/AbstractCleaningCommand.java | 103 +++ .../commands/AbstractNoResponseCommand.java | 41 + .../commands/CustomAreaCleaningCommand.java | 25 + .../api/commands/EmptyDustbinCommand.java | 45 + .../api/commands/GetActiveMapIdCommand.java | 52 ++ .../api/commands/GetBatteryInfoCommand.java | 52 ++ .../api/commands/GetChargeStateCommand.java | 53 ++ .../api/commands/GetCleanLogsCommand.java | 85 ++ .../api/commands/GetCleanStateCommand.java | 72 ++ .../commands/GetComponentLifeSpanCommand.java | 86 ++ .../GetContinuousCleaningCommand.java | 59 ++ .../GetDefaultCleanPassesCommand.java | 48 + .../commands/GetDustbinAutoEmptyCommand.java | 48 + .../api/commands/GetErrorCommand.java | 56 ++ .../commands/GetFirmwareVersionCommand.java | 54 ++ .../GetMapSpotAreasWithMapIdCommand.java | 82 ++ .../GetMoppingWaterAmountCommand.java | 53 ++ .../api/commands/GetNetworkInfoCommand.java | 60 ++ .../api/commands/GetSuctionPowerCommand.java | 52 ++ .../api/commands/GetTotalStatsCommand.java | 72 ++ .../api/commands/GetTrueDetectCommand.java | 48 + .../api/commands/GetVolumeCommand.java | 61 ++ .../GetWaterSystemPresentCommand.java | 52 ++ .../api/commands/GoChargingCommand.java | 51 ++ .../api/commands/IotDeviceCommand.java | 87 ++ .../api/commands/PauseCleaningCommand.java | 26 + .../api/commands/PlaySoundCommand.java | 96 ++ .../api/commands/ResumeCleaningCommand.java | 26 + .../SetContinuousCleaningCommand.java | 53 ++ .../SetDefaultCleanPassesCommand.java | 50 ++ .../commands/SetDustbinAutoEmptyCommand.java | 47 + .../SetMoppingWaterAmountCommand.java | 53 ++ .../api/commands/SetSuctionPowerCommand.java | 52 ++ .../api/commands/SetTrueDetectCommand.java | 47 + .../api/commands/SetVolumeCommand.java | 50 ++ .../api/commands/SpotAreaCleaningCommand.java | 27 + .../commands/StartAutoCleaningCommand.java | 26 + .../api/commands/StopCleaningCommand.java | 26 + .../internal/api/impl/DeviceDescription.java | 63 ++ .../internal/api/impl/EcovacsApiImpl.java | 361 ++++++++ .../api/impl/EcovacsApiUrlFactory.java | 74 ++ .../internal/api/impl/EcovacsIotMqDevice.java | 211 +++++ .../internal/api/impl/EcovacsXmppDevice.java | 467 ++++++++++ .../internal/api/impl/JsonReportParser.java | 149 ++++ .../ecovacs/internal/api/impl/PortalTodo.java | 28 + .../internal/api/impl/ProtocolVersion.java | 30 + .../internal/api/impl/ReportParser.java | 24 + .../internal/api/impl/XmlReportParser.java | 103 +++ .../dto/request/portal/PortalAuthRequest.java | 38 + .../portal/PortalAuthRequestParameter.java | 44 + .../portal/PortalCleanLogsRequest.java | 39 + .../portal/PortalIotCommandRequest.java | 71 ++ .../portal/PortalIotProductRequest.java | 34 + .../request/portal/PortalLoginRequest.java | 63 ++ .../deviceapi/json/BatteryReport.java | 25 + .../deviceapi/json/CachedMapInfoReport.java | 39 + .../response/deviceapi/json/ChargeReport.java | 25 + .../response/deviceapi/json/CleanReport.java | 55 ++ .../deviceapi/json/CleanReportV2.java | 60 ++ .../json/ComponentLifeSpanReport.java | 29 + .../json/DefaultCleanCountReport.java | 20 + .../deviceapi/json/EnabledStateReport.java | 23 + .../response/deviceapi/json/ErrorReport.java | 25 + .../response/deviceapi/json/EventReport.java | 23 + .../response/deviceapi/json/MapSetReport.java | 35 + .../deviceapi/json/NetworkInfoReport.java | 23 + .../response/deviceapi/json/SleepReport.java | 23 + .../response/deviceapi/json/SpeedReport.java | 23 + .../response/deviceapi/json/StatsReport.java | 31 + .../deviceapi/json/WaterInfoReport.java | 25 + .../response/deviceapi/xml/CleaningInfo.java | 78 ++ .../response/deviceapi/xml/DeviceInfo.java | 95 ++ .../deviceapi/xml/WaterSystemInfo.java | 42 + .../impl/dto/response/main/AccessData.java | 89 ++ .../api/impl/dto/response/main/AuthCode.java | 40 + .../dto/response/main/ResponseWrapper.java | 63 ++ .../AbstractPortalIotCommandResponse.java | 51 ++ .../portal/AbstractPortalResponse.java | 33 + .../api/impl/dto/response/portal/Device.java | 88 ++ .../impl/dto/response/portal/IotProduct.java | 102 +++ .../portal/PortalCleanLogsResponse.java | 66 ++ .../response/portal/PortalDeviceResponse.java | 35 + .../portal/PortalIotCommandJsonResponse.java | 90 ++ .../portal/PortalIotCommandXmlResponse.java | 32 + .../portal/PortalIotProductResponse.java | 35 + .../response/portal/PortalLoginResponse.java | 57 ++ .../api/impl/dto/response/portal/Service.java | 40 + .../internal/api/model/ChargeMode.java | 32 + .../internal/api/model/CleanLogRecord.java | 38 + .../ecovacs/internal/api/model/CleanMode.java | 57 ++ .../ecovacs/internal/api/model/Component.java | 34 + .../internal/api/model/DeviceCapability.java | 54 ++ .../api/model/MoppingWaterAmount.java | 34 + .../internal/api/model/NetworkInfo.java | 33 + .../internal/api/model/SpotAreaType.java | 52 ++ .../internal/api/model/SuctionPower.java | 63 ++ .../api/util/DataParsingException.java | 31 + .../ecovacs/internal/api/util/MD5Util.java | 53 ++ .../internal/api/util/SchedulerTask.java | 97 +++ .../ecovacs/internal/api/util/XPathUtils.java | 67 ++ .../config/EcovacsApiConfiguration.java | 28 + .../config/EcovacsVacuumConfiguration.java | 26 + .../EcovacsDeviceDiscoveryService.java | 151 ++++ .../internal/handler/EcovacsApiHandler.java | 153 ++++ .../handler/EcovacsVacuumHandler.java | 822 ++++++++++++++++++ .../internal/util/StateOptionEntry.java | 41 + .../internal/util/StateOptionMapping.java | 49 ++ .../src/main/resources/OH-INF/addon/addon.xml | 11 + .../resources/OH-INF/i18n/ecovacs.properties | 179 ++++ .../main/resources/OH-INF/thing/bridge.xml | 38 + .../resources/OH-INF/thing/thing-types.xml | 355 ++++++++ .../devices/supported_device_list.json | 541 ++++++++++++ bundles/pom.xml | 1 + 128 files changed, 9421 insertions(+) create mode 100644 bundles/org.openhab.binding.ecovacs/NOTICE create mode 100644 bundles/org.openhab.binding.ecovacs/README.md create mode 100644 bundles/org.openhab.binding.ecovacs/pom.xml create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsBindingConstants.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsDynamicStateDescriptionProvider.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsHandlerFactory.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/action/EcovacsVacuumActions.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApi.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApiConfiguration.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApiException.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsDevice.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractAreaCleaningCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractCleaningCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractNoResponseCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/CustomAreaCleaningCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/EmptyDustbinCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetActiveMapIdCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetBatteryInfoCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetChargeStateCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetCleanLogsCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetCleanStateCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetComponentLifeSpanCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetContinuousCleaningCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetDefaultCleanPassesCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetDustbinAutoEmptyCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetErrorCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetFirmwareVersionCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetMapSpotAreasWithMapIdCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetMoppingWaterAmountCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetNetworkInfoCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetSuctionPowerCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetTotalStatsCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetTrueDetectCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetVolumeCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetWaterSystemPresentCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GoChargingCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/IotDeviceCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/PauseCleaningCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/PlaySoundCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/ResumeCleaningCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetContinuousCleaningCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetDefaultCleanPassesCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetDustbinAutoEmptyCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetMoppingWaterAmountCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetSuctionPowerCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetTrueDetectCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetVolumeCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SpotAreaCleaningCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/StartAutoCleaningCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/StopCleaningCommand.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/DeviceDescription.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiImpl.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiUrlFactory.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsIotMqDevice.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsXmppDevice.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/JsonReportParser.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/PortalTodo.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/ProtocolVersion.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/ReportParser.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/XmlReportParser.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalAuthRequest.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalAuthRequestParameter.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalCleanLogsRequest.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalIotCommandRequest.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalIotProductRequest.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalLoginRequest.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/BatteryReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CachedMapInfoReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ChargeReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CleanReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CleanReportV2.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ComponentLifeSpanReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/DefaultCleanCountReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/EnabledStateReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ErrorReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/EventReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/MapSetReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/NetworkInfoReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/SleepReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/SpeedReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/StatsReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/WaterInfoReport.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/CleaningInfo.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/DeviceInfo.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/WaterSystemInfo.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/AccessData.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/AuthCode.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/ResponseWrapper.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/AbstractPortalIotCommandResponse.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/AbstractPortalResponse.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/Device.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/IotProduct.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanLogsResponse.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalDeviceResponse.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotCommandJsonResponse.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotCommandXmlResponse.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotProductResponse.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalLoginResponse.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/Service.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/ChargeMode.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/CleanLogRecord.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/CleanMode.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/Component.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/DeviceCapability.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/MoppingWaterAmount.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/NetworkInfo.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/SpotAreaType.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/SuctionPower.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/DataParsingException.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/MD5Util.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/SchedulerTask.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/XPathUtils.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/config/EcovacsApiConfiguration.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/config/EcovacsVacuumConfiguration.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/discovery/EcovacsDeviceDiscoveryService.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/handler/EcovacsApiHandler.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/handler/EcovacsVacuumHandler.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/util/StateOptionEntry.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/util/StateOptionMapping.java create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/i18n/ecovacs.properties create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/thing/bridge.xml create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.ecovacs/src/main/resources/devices/supported_device_list.json diff --git a/CODEOWNERS b/CODEOWNERS index 5be07e4077702..c2270388ba127 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -82,6 +82,7 @@ /bundles/org.openhab.binding.echonetlite/ @mikeb01 /bundles/org.openhab.binding.ecobee/ @mhilbush /bundles/org.openhab.binding.ecotouch/ @sibbi77 +/bundles/org.openhab.binding.ecovacs/ @maniac103 /bundles/org.openhab.binding.ecowatt/ @lolodomo /bundles/org.openhab.binding.ekey/ @hmerk /bundles/org.openhab.binding.electroluxair/ @jannegpriv diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index d385f693c87fa..d968a3512444d 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -406,6 +406,11 @@ org.openhab.binding.ecotouch ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.ecovacs + ${project.version} + org.openhab.addons.bundles org.openhab.binding.ecowatt diff --git a/bundles/org.openhab.binding.ecovacs/NOTICE b/bundles/org.openhab.binding.ecovacs/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.ecovacs/README.md b/bundles/org.openhab.binding.ecovacs/README.md new file mode 100644 index 0000000000000..98284ba23de56 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/README.md @@ -0,0 +1,175 @@ +# Ecovacs Binding + +This binding provides integration for vacuum cleaning / mopping robots made by Ecovacs (). +It discovers devices and communicates to them by using Ecovacs' cloud services. + +## Supported Things + +- Ecovacs cloud API (`ecovacsapi`) +- Vacuum cleaner (`vacuum`) + +At this point, the following devices are fully supported and verified to be working: + +- Deebot OZMO 900/905 +- Deebot OZMO 920 +- Deebot OZMO 930 +- Deebot OZMO 950 +- Deebot OZMO Slim 10/11 +- Deebot N8 series + +The following devices will likely work because they are using similar protocols as the above ones: + +- Deebot 600/601/605 +- Deebot 900/901 +- Deebot OZMO 610 +- Deebot 710/711/711s +- Deebot OZMO T5 +- Deebot (OZMO) T8 series +- Deebot T9 series +- Deebot Slim 2 +- Deebot N3 MAX +- Deebot N7 +- Deebot U2 series +- Deebot X1 Omni + +## Discovery + +At first, you need to manually create the bridge thing for the cloud API. +Once that is done, the supported devices will be automatically discovered and added to the inbox. + +## Thing Configuration + +For the cloud API thing, the following parameters must be configured: + +| Config | Description | +|-----------|-------------------------------------------------------------------------------------------------------------------------------| +| email | The email address you used when registering the Ecovacs cloud account | +| password | The cloud account password | +| continent | The continent you are residing on, or 'World' if none matches. This is used to select the correct cloud server to connect to. | + +For the vacuum things, there is no required configuration (when using discovery). The following parameters exist: + +| Config | Description | +|--------------|-------------------------------------------------------------------------------------------------------------------------------| +| serialNumber | Required: The device's serial number as printed on the barcode below the dust bin. Filled automatically when using discovery. | +| refresh | Refresh interval for polled data (see below), in minutes. By default set to 5 minutes. | + +## Channels + +The list below lists all channels supported by the binding. +In case a particular channel is not supported by a given device (see remarks), it is automatically removed from the given thing. + +| Channel | Type | Description | Read Only | Updated By | Remarks | +|-----------------------------------------|----------------------|-----------------------------------------------------------|-----------|------------|----------| +| actions#command | String | Command to execute | No | Event | [1] | +| status#state | String | Current operational state | Yes | Event | [2] | +| status#current-cleaning-mode | String | Mode used in current cleaning run | Yes | Event | [3], [4] | +| status#current-cleaning-time | Number:Time | Time spent in current cleaning run | Yes | Event | [4] | +| status#current-cleaned-area | Number:Area | Area cleaned in current cleaning run | Yes | Event | [4] | +| status#current-cleaning-spot-definition | String | The spot to clean in current cleaning run | Yes | Event | [4], [5] | +| status#water-system-present | Switch | Whether the device is currently ready for mopping | Yes | Event | [6] | +| status#wifi-rssi | Number:Power | The current Wi-Fi signal strength of the device | Yes | Polling | [7] | +| consumables#main-brush-lifetime | Number:Dimensionless | The remaining life time of the main brush in percent | Yes | Polling | [8] | +| consumables#side-brush-lifetime | Number:Dimensionless | The remaining life time of the side brush in percent | Yes | Polling | | +| consumables#dust-filter-lifetime | Number:Dimensionless | The remaining life time of the dust bin filter in percent | Yes | Polling | | +| consumables#other-component-lifetime | Number:Dimensionless | The remaining time until device maintenance in percent | Yes | Polling | [9] | +| last-clean#last-clean-start | DateTime | The start time of the last completed cleaning run | Yes | Polling | | +| last-clean#last-clean-duration | Number:Time | The duration of the last completed cleaning run | Yes | Polling | | +| last-clean#last-clean-area | Number:Area | The area cleaned in the last completed cleaning run | Yes | Polling | | +| last-clean#last-clean-mode | String | The mode used for the last completed cleaning run | Yes | Polling | [3] | +| last-clean#last-clean-map | Image | The map image of the last completed cleaning run | Yes | Polling | | +| total-stats#total-cleaning-time | Number:Time | The total time spent cleaning during the device life time | Yes | Polling | | +| total-stats#total-cleaned-area | Number:Area | The total area cleaned during the device life time | Yes | Polling | | +| total-stats#total-clean-runs | Number | The total number of clean runs in the device life time | Yes | Polling | | +| settings#auto-empty | Switch | Whether dust bin auto empty to station is enabled | No | Polling | [10] | +| settings#cleaning-passes | Number | Number of cleaning passes to be used (1 or 2) | No | Polling | [9] | +| settings#continuous-cleaning | Switch | Whether unfinished cleaning resumes after charging | No | Polling | | +| settings#suction-power | String | The power level used during cleaning | No | Polling | [11] | +| settings#true-detect-3d | Switch | Whether True Detect 3D is enabled | No | Polling | [12] | +| settings#voice-volume | Dimmer | The voice volume level in percent | No | Polling | [13] | +| settings#water-amount | String | The amount of water to be used when mopping | No | Polling | [14] | + +Remarks: + +- [1] See [section below](#command-channel-actions) +- [2] Possible states: 'cleaning', 'pause', 'stop', 'drying', 'washing', 'returning' and 'charging' (where 'drying' and 'washing' are only available on newer models with auto empty station) +- [3] Possible states: 'auto', 'edge', 'spot', 'spotArea', 'customArea', 'singleRoom' (some of which depend on device capabilities) +- [4] Current cleaning status is only valid if the device is currently cleaning +- [5] Only valid for 'spot', 'spotArea' and 'customArea' cleaning modes; value can be used for 'spotArea' and 'customArea' commands (see below) +- [6] Only present if device has a mopping system +- [7] Only present on newer generation devices (Deebot OZMO 950 and newer) +- [8] Only present if device has a main brush +- [9] Only present on newer generation devices (Deebot N8/T8 or newer) +- [10] Only present if device has a dustbin auto empty station; supports both on/off command (to turn on/off the setting) and the string 'trigger' (to trigger immediate auto empty) +- [11] Only present if device can control power level. Possible values vary by device: 'normal' and 'high' are always supported, 'silent' and 'higher' are supported for some models +- [12] Only present if device supports True Detect 3D +- [13] Only present if device has voice reporting +- [14] Only present if device has a mopping system. Possible values include 'low', 'medium', 'high' and 'veryhigh' + +## Command Channel Actions + +The following actions are supported by the `command` channel: + +| Name | Action | Remarks | +|--------------|-------------------------------------------|------------------------------------------------------| +| `clean` | Start cleaning in automatic mode. | | +| `spotArea` | Start cleaning specific rooms. |
  • Only if supported by device, which can be recognized by `spotArea` being present in the list of possible states of the `current-cleaning-mode` channel.
  • Format: `spotArea:`, where `room IDs` is a semicolon separated list of room letters as shown in Ecovacs' app, so a valid command could e.g. be `spotArea:A;D;E`.
  • If you want to run 2 clean passes, amend `:x2` to the command, e.g. `spotArea:A;C;B:x2`.
| +| `customArea` | Start cleaning specific areas. |
  • Only if supported by device, which can be recognized by `customArea` being present in the list of possible states of the `current-cleaning-mode` channel.
  • Format: `customArea:;;;, where the parameters are coordinates (in mm) relative to the map.
  • The coordinates can be obtained from the `current-cleaning-spot-definition` channel when starting a custom area run from the app.
  • If you want to run 2 clean passes, amend `:x2` to the command, e.g. `customArea:100;100;1000;1000:x2`.
| +| `pause` | Pause cleaning if it's currently active. | If the device is idle, the command is ignored. | +| `resume` | Resume cleaning if it's currently paused. | If the device is not paused, the command is ignored. | +| `stop` | Stop cleaning immediately. | | +| `charge` | Send device to charging station. | | + +## Rule actions + +This binding includes a rule action, which allows playback of specific sounds on the device in case the device has a speaker. +There is a separate instance for each device, which can be retrieved like this: + +```java +val vacuumActions = getActions("ecovacs","ecovacs:vacuum:1234567890") +``` + +where the first parameter always has to be `ecovacs` and the second is the full Thing UID of the device that should be used. +Once this action instance is retrieved, you can invoke the `playSound(String type)` method on it: + +```java +vacuumActions.playSound("beep") +``` + +Supported sound types include: + +- `beep` +- `iAmHere` +- `startup` +- `suspended` +- `batteryLow` + +For special use cases, there is also a `playSoundWithId(int soundId)` method, where you can pass the numeric ID of the sound to play. +The exact meaning of the number depends on the specific device; you'll need to experiment with different numbers to see how the number-to-sound mapping looks like. +For reference, a list for the Deebot 900 can be found [here](https://github.com/bmartin5692/sucks/blob/D901/protocol.md#user-content-sounds). + +## File Based Configuration + +If you want to create the API bridge in a .things file, the entry has to look as follows: + +```java +Bridge ecovacs:ecovacsapi:ecovacsapi [ email="your.email@provider.com", password="yourpassword", continent="ww" ] +``` + +The possible values for `continent` include the following values: + +- `ww` for World +- `eu` for Europe +- `na` for North America +- `as` for Asia + +The devices are detected automatically. +If you also want to enter those manually, the syntax is as follows: + +```java +Bridge ecovacs:ecovacsapi:ecovacsapi [ email="your.email@provider.com", password="yourpassword", continent="ww" ] +{ + Thing vacuum myDeebot "Deebot Vacuum" [ serialNumber="serial as printed on label below dust bin" ] +} +``` + diff --git a/bundles/org.openhab.binding.ecovacs/pom.xml b/bundles/org.openhab.binding.ecovacs/pom.xml new file mode 100644 index 0000000000000..fe9554e77841c --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/pom.xml @@ -0,0 +1,51 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.0.0-SNAPSHOT + + + org.openhab.binding.ecovacs + + openHAB Add-ons :: Bundles :: Ecovacs Binding + + 4.3.3 + + + + + org.igniterealtime.smack + smack-tcp + ${smack.version} + provided + + + org.igniterealtime.smack + smack-im + ${smack.version} + provided + + + org.igniterealtime.smack + smack-extensions + ${smack.version} + provided + + + org.igniterealtime.smack + smack-java7 + ${smack.version} + provided + + + org.igniterealtime.smack + smack-resolver-javax + ${smack.version} + + + diff --git a/bundles/org.openhab.binding.ecovacs/src/main/feature/feature.xml b/bundles/org.openhab.binding.ecovacs/src/main/feature/feature.xml new file mode 100644 index 0000000000000..ee9c3121e6334 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/feature/feature.xml @@ -0,0 +1,22 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab.tp-hivemqclient + mvn:org.igniterealtime.smack/smack-tcp/4.3.3 + mvn:org.jxmpp/jxmpp-core/0.6.3 + mvn:org.jxmpp/jxmpp-jid/0.6.3 + mvn:org.jxmpp/jxmpp-util-cache/0.6.3 + mvn:org.minidns/minidns-core/0.3.3 + mvn:org.igniterealtime.smack/smack-core/4.3.3 + mvn:org.igniterealtime.smack/smack-im/4.3.3 + mvn:org.igniterealtime.smack/smack-extensions/4.3.3 + mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.xpp3/1.1.4c_7 + mvn:org.igniterealtime.smack/smack-resolver-javax/4.3.3 + mvn:org.igniterealtime.smack/smack-java7/4.3.3 + mvn:org.igniterealtime.smack/smack-sasl-javax/4.3.3 + mvn:org.openhab.addons.bundles/org.openhab.binding.ecovacs/${project.version} + + diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsBindingConstants.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsBindingConstants.java new file mode 100644 index 0000000000000..a805ea83a67ea --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsBindingConstants.java @@ -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 CLEAN_MODE_MAPPING = StateOptionMapping. of( + new StateOptionEntry(CleanMode.AUTO, "auto"), + new StateOptionEntry(CleanMode.EDGE, "edge", DeviceCapability.EDGE_CLEANING), + new StateOptionEntry(CleanMode.SPOT, "spot", DeviceCapability.SPOT_CLEANING), + new StateOptionEntry(CleanMode.SPOT_AREA, "spotArea", DeviceCapability.SPOT_AREA_CLEANING), + new StateOptionEntry(CleanMode.CUSTOM_AREA, "customArea", DeviceCapability.CUSTOM_AREA_CLEANING), + new StateOptionEntry(CleanMode.SINGLE_ROOM, "singleRoom", DeviceCapability.SINGLE_ROOM_CLEANING), + new StateOptionEntry(CleanMode.PAUSE, "pause"), + new StateOptionEntry(CleanMode.STOP, "stop"), + new StateOptionEntry(CleanMode.WASHING, "washing"), + new StateOptionEntry(CleanMode.DRYING, "drying"), + new StateOptionEntry(CleanMode.RETURNING, "returning")); + + public static final StateOptionMapping WATER_AMOUNT_MAPPING = StateOptionMapping + . of(new StateOptionEntry(MoppingWaterAmount.LOW, "low"), + new StateOptionEntry(MoppingWaterAmount.MEDIUM, "medium"), + new StateOptionEntry(MoppingWaterAmount.HIGH, "high"), + new StateOptionEntry(MoppingWaterAmount.VERY_HIGH, "veryhigh")); + + public static final StateOptionMapping SUCTION_POWER_MAPPING = StateOptionMapping. of( + new StateOptionEntry(SuctionPower.SILENT, "silent", + DeviceCapability.EXTENDED_CLEAN_SPEED_CONTROL), + new StateOptionEntry(SuctionPower.NORMAL, "normal"), + new StateOptionEntry(SuctionPower.HIGH, "high"), new StateOptionEntry( + SuctionPower.HIGHER, "higher", DeviceCapability.EXTENDED_CLEAN_SPEED_CONTROL)); + + public static final StateOptionMapping SOUND_TYPE_MAPPING = StateOptionMapping. of( + new StateOptionEntry(SoundType.BEEP, "beep"), + new StateOptionEntry(SoundType.I_AM_HERE, "iAmHere"), + new StateOptionEntry(SoundType.STARTUP, "startup"), + new StateOptionEntry(SoundType.SUSPENDED, "suspended"), + new StateOptionEntry(SoundType.BATTERY_LOW, "batteryLow")); +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsDynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsDynamicStateDescriptionProvider.java new file mode 100644 index 0000000000000..a7a1190dcea52 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsDynamicStateDescriptionProvider.java @@ -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 localizedStateOptions(List 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); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsHandlerFactory.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsHandlerFactory.java new file mode 100644 index 0000000000000..dee2533f7599f --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/EcovacsHandlerFactory.java @@ -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 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/action/EcovacsVacuumActions.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/action/EcovacsVacuumActions.java new file mode 100644 index 0000000000000..1d0a941662ef9 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/action/EcovacsVacuumActions.java @@ -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 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApi.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApi.java new file mode 100644 index 0000000000000..b226bb636124c --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApi.java @@ -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 getDevices() throws EcovacsApiException, InterruptedException; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApiConfiguration.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApiConfiguration.java new file mode 100644 index 0000000000000..9019c6804eed4 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApiConfiguration.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApiException.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApiException.java new file mode 100644 index 0000000000000..aaf8cf2d35bd3 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsApiException.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsDevice.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsDevice.java new file mode 100644 index 0000000000000..e74e3f4964573 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/EcovacsDevice.java @@ -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 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 sendCommand(IotDeviceCommand command) throws EcovacsApiException, InterruptedException; + + List getCleanLogs() throws EcovacsApiException, InterruptedException; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractAreaCleaningCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractAreaCleaningCommand.java new file mode 100644 index 0000000000000..fb319d711c021 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractAreaCleaningCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractCleaningCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractCleaningCommand.java new file mode 100644 index 0000000000000..aa796bec53071 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractCleaningCommand.java @@ -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 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 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(); + } + }); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractNoResponseCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractNoResponseCommand.java new file mode 100644 index 0000000000000..d0f6ffff36d3a --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/AbstractNoResponseCommand.java @@ -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 { + 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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/CustomAreaCleaningCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/CustomAreaCleaningCommand.java new file mode 100644 index 0000000000000..bb548091a9a74 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/CustomAreaCleaningCommand.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/EmptyDustbinCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/EmptyDustbinCommand.java new file mode 100644 index 0000000000000..6e097d7683bde --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/EmptyDustbinCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetActiveMapIdCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetActiveMapIdCommand.java new file mode 100644 index 0000000000000..943846afdc129 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetActiveMapIdCommand.java @@ -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 { + 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(); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetBatteryInfoCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetBatteryInfoCommand.java new file mode 100644 index 0000000000000..9b5b0cdcf42f0 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetBatteryInfoCommand.java @@ -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 { + 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetChargeStateCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetChargeStateCommand.java new file mode 100644 index 0000000000000..ebf79af19d02c --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetChargeStateCommand.java @@ -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 { + 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetCleanLogsCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetCleanLogsCommand.java new file mode 100644 index 0000000000000..5d2f419d472e6 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetCleanLogsCommand.java @@ -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> { + 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 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 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetCleanStateCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetCleanStateCommand.java new file mode 100644 index 0000000000000..66b23e65238ef --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetCleanStateCommand.java @@ -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 { + 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; + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetComponentLifeSpanCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetComponentLifeSpanCommand.java new file mode 100644 index 0000000000000..92dbaed614635 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetComponentLifeSpanCommand.java @@ -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 { + 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>() { + }.getType(); + try { + List 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetContinuousCleaningCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetContinuousCleaningCommand.java new file mode 100644 index 0000000000000..7c12ccee26d8c --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetContinuousCleaningCommand.java @@ -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 { + 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetDefaultCleanPassesCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetDefaultCleanPassesCommand.java new file mode 100644 index 0000000000000..1de42221a4bdd --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetDefaultCleanPassesCommand.java @@ -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 { + 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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetDustbinAutoEmptyCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetDustbinAutoEmptyCommand.java new file mode 100644 index 0000000000000..e44f844382d62 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetDustbinAutoEmptyCommand.java @@ -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 { + 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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetErrorCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetErrorCommand.java new file mode 100644 index 0000000000000..fac4743c9bcfc --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetErrorCommand.java @@ -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> { + public GetErrorCommand() { + super(); + } + + @Override + public String getName(ProtocolVersion version) { + return version == ProtocolVersion.XML ? "GetError" : "getError"; + } + + @Override + public Optional 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetFirmwareVersionCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetFirmwareVersionCommand.java new file mode 100644 index 0000000000000..375eeb860a761 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetFirmwareVersionCommand.java @@ -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 { + 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(); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetMapSpotAreasWithMapIdCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetMapSpotAreasWithMapIdCommand.java new file mode 100644 index 0000000000000..496fbb49fa8fb --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetMapSpotAreasWithMapIdCommand.java @@ -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> { + 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 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 result = new ArrayList<>(); + for (int i = 0; i < mapIds.getLength(); i++) { + result.add(mapIds.item(i).getNodeValue()); + } + return result; + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetMoppingWaterAmountCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetMoppingWaterAmountCommand.java new file mode 100644 index 0000000000000..e300e9879cf88 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetMoppingWaterAmountCommand.java @@ -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 { + 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetNetworkInfoCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetNetworkInfoCommand.java new file mode 100644 index 0000000000000..4c23ca375dd3c --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetNetworkInfoCommand.java @@ -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 { + 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetSuctionPowerCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetSuctionPowerCommand.java new file mode 100644 index 0000000000000..45b86d8ead647 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetSuctionPowerCommand.java @@ -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 { + 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetTotalStatsCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetTotalStatsCommand.java new file mode 100644 index 0000000000000..04179c1c5425b --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetTotalStatsCommand.java @@ -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 { + 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); + } + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetTrueDetectCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetTrueDetectCommand.java new file mode 100644 index 0000000000000..f3f5416e84b6e --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetTrueDetectCommand.java @@ -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 { + 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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetVolumeCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetVolumeCommand.java new file mode 100644 index 0000000000000..fc75720425c54 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetVolumeCommand.java @@ -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 { + 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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetWaterSystemPresentCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetWaterSystemPresentCommand.java new file mode 100644 index 0000000000000..55e5d3265223f --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GetWaterSystemPresentCommand.java @@ -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 { + 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GoChargingCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GoChargingCommand.java new file mode 100644 index 0000000000000..0aac16d32bf07 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/GoChargingCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/IotDeviceCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/IotDeviceCommand.java new file mode 100644 index 0000000000000..611b2bdd7a005 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/IotDeviceCommand.java @@ -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 { + 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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/PauseCleaningCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/PauseCleaningCommand.java new file mode 100644 index 0000000000000..898784a66624a --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/PauseCleaningCommand.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/PlaySoundCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/PlaySoundCommand.java new file mode 100644 index 0000000000000..36725293ca0c0 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/PlaySoundCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/ResumeCleaningCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/ResumeCleaningCommand.java new file mode 100644 index 0000000000000..1ff6ddaa4ea3a --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/ResumeCleaningCommand.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetContinuousCleaningCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetContinuousCleaningCommand.java new file mode 100644 index 0000000000000..edadfd8006043 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetContinuousCleaningCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetDefaultCleanPassesCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetDefaultCleanPassesCommand.java new file mode 100644 index 0000000000000..b58a3c80440a3 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetDefaultCleanPassesCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetDustbinAutoEmptyCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetDustbinAutoEmptyCommand.java new file mode 100644 index 0000000000000..ea4c4ba0cbf3b --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetDustbinAutoEmptyCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetMoppingWaterAmountCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetMoppingWaterAmountCommand.java new file mode 100644 index 0000000000000..5488f21b6b73f --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetMoppingWaterAmountCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetSuctionPowerCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetSuctionPowerCommand.java new file mode 100644 index 0000000000000..f498eec1ed835 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetSuctionPowerCommand.java @@ -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()); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetTrueDetectCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetTrueDetectCommand.java new file mode 100644 index 0000000000000..3d977e528b16b --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetTrueDetectCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetVolumeCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetVolumeCommand.java new file mode 100644 index 0000000000000..8cf64ba28d585 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SetVolumeCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SpotAreaCleaningCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SpotAreaCleaningCommand.java new file mode 100644 index 0000000000000..9a693cfaa5ca6 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/SpotAreaCleaningCommand.java @@ -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 roomIds, int cleanPasses) { + super("spotArea", String.join(",", roomIds), cleanPasses); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/StartAutoCleaningCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/StartAutoCleaningCommand.java new file mode 100644 index 0000000000000..999a32dd4bf02 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/StartAutoCleaningCommand.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/StopCleaningCommand.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/StopCleaningCommand.java new file mode 100644 index 0000000000000..369c40738cbf5 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/commands/StopCleaningCommand.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/DeviceDescription.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/DeviceDescription.java new file mode 100644 index 0000000000000..c8792abfe301a --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/DeviceDescription.java @@ -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 capabilities; + + public DeviceDescription(String modelName, String deviceClass, @Nullable String deviceClassLink, + ProtocolVersion protoVersion, boolean usesMqtt, Set 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiImpl.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiImpl.java new file mode 100644 index 0000000000000..2cf1f784b9833 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiImpl.java @@ -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 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>() { + }.getType(); + return handleResponseWrapper(gson.fromJson(loginResponse.getContentAsString(), responseType)); + } + + private AuthCode getAuthCode(AccessData accessData) throws EcovacsApiException, InterruptedException { + HashMap 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>() { + }.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 getDevices() throws EcovacsApiException, InterruptedException { + List descriptions = getSupportedDeviceList(); + List products = null; + List devices = new ArrayList<>(); + for (Device dev : getDeviceList()) { + Optional 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 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>() { + }.getType(); + List descs = gson.fromJson(reader, type); + return descs.stream().map(desc -> { + final DeviceDescription result; + if (desc.deviceClassLink != null) { + Optional 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 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 devices = handleResponse(deviceResponse, PortalDeviceResponse.class).getDevices(); + return devices != null ? devices : Collections.emptyList(); + } + + private List 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 products = handleResponse(productResponse, PortalIotProductResponse.class).getProducts(); + return products != null ? products : Collections.emptyList(); + } + + public T sendIotCommand(Device device, DeviceDescription desc, IotDeviceCommand 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 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 handleResponseWrapper(@Nullable ResponseWrapper 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 handleResponse(ContentResponse response, Class 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 requestSpecificParameters) { + HashMap 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiUrlFactory.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiUrlFactory.java new file mode 100644 index 0000000000000..745bf37b6d5a8 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsApiUrlFactory.java @@ -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"; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsIotMqDevice.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsIotMqDevice.java new file mode 100644 index 0000000000000..fe57fd3f2306a --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsIotMqDevice.java @@ -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 sendCommand(IotDeviceCommand command) throws EcovacsApiException, InterruptedException { + return api.sendIotCommand(device, desc, command); + } + + @Override + public List getCleanLogs() throws EcovacsApiException, InterruptedException { + Stream 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() }; + } + }; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsXmppDevice.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsXmppDevice.java new file mode 100644 index 0000000000000..193ed6ad993b5 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/EcovacsXmppDevice.java @@ -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 sendCommand(IotDeviceCommand 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 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 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 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()); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/JsonReportParser.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/JsonReportParser.java new file mode 100644 index 0000000000000..0433ea478b49d --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/JsonReportParser.java @@ -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 payloadAs(JsonResponsePayloadWrapper response, Class 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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/PortalTodo.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/PortalTodo.java new file mode 100644 index 0000000000000..13987989ff0aa --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/PortalTodo.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/ProtocolVersion.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/ProtocolVersion.java new file mode 100644 index 0000000000000..10486b323caae --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/ProtocolVersion.java @@ -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 +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/ReportParser.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/ReportParser.java new file mode 100644 index 0000000000000..53db9eefa2344 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/ReportParser.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/XmlReportParser.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/XmlReportParser.java new file mode 100644 index 0000000000000..be9f7a0b7ec8f --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/XmlReportParser.java @@ -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: + // + 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: + // + 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; + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalAuthRequest.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalAuthRequest.java new file mode 100644 index 0000000000000..a7b364be74ece --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalAuthRequest.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalAuthRequestParameter.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalAuthRequestParameter.java new file mode 100644 index 0000000000000..036d7b0986a06 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalAuthRequestParameter.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalCleanLogsRequest.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalCleanLogsRequest.java new file mode 100644 index 0000000000000..0d41c9de2388f --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalCleanLogsRequest.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalIotCommandRequest.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalIotCommandRequest.java new file mode 100644 index 0000000000000..ef69996a52eef --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalIotCommandRequest.java @@ -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(); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalIotProductRequest.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalIotProductRequest.java new file mode 100644 index 0000000000000..9a159094b07fb --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalIotProductRequest.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalLoginRequest.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalLoginRequest.java new file mode 100644 index 0000000000000..33c6edff5afb6 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/request/portal/PortalLoginRequest.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/BatteryReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/BatteryReport.java new file mode 100644 index 0000000000000..106107bea1627 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/BatteryReport.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CachedMapInfoReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CachedMapInfoReport.java new file mode 100644 index 0000000000000..ccc09a3326b21 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CachedMapInfoReport.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ChargeReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ChargeReport.java new file mode 100644 index 0000000000000..026b966f35ce1 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ChargeReport.java @@ -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, ...? +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CleanReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CleanReport.java new file mode 100644 index 0000000000000..faed6211fb50c --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CleanReport.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CleanReportV2.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CleanReportV2.java new file mode 100644 index 0000000000000..4d87fd5ba6273 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/CleanReportV2.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ComponentLifeSpanReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ComponentLifeSpanReport.java new file mode 100644 index 0000000000000..78c74b127a9c8 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ComponentLifeSpanReport.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/DefaultCleanCountReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/DefaultCleanCountReport.java new file mode 100644 index 0000000000000..81edf38f559ad --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/DefaultCleanCountReport.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/EnabledStateReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/EnabledStateReport.java new file mode 100644 index 0000000000000..ef75864683c31 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/EnabledStateReport.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ErrorReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ErrorReport.java new file mode 100644 index 0000000000000..cfd96caa6528b --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/ErrorReport.java @@ -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 errorCodes; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/EventReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/EventReport.java new file mode 100644 index 0000000000000..81ba730c70288 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/EventReport.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/MapSetReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/MapSetReport.java new file mode 100644 index 0000000000000..d6ce3a0f9bf98 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/MapSetReport.java @@ -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 subsets; + + public static class MapSubSetInfo { + @SerializedName("mssid") + public String id; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/NetworkInfoReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/NetworkInfoReport.java new file mode 100644 index 0000000000000..0123f7c65cb9c --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/NetworkInfoReport.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/SleepReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/SleepReport.java new file mode 100644 index 0000000000000..955745f29f2cc --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/SleepReport.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/SpeedReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/SpeedReport.java new file mode 100644 index 0000000000000..d81da0c0a5f4e --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/SpeedReport.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/StatsReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/StatsReport.java new file mode 100644 index 0000000000000..0b38aa0a93f37 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/StatsReport.java @@ -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, ... ? +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/WaterInfoReport.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/WaterInfoReport.java new file mode 100644 index 0000000000000..412f79eea2087 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/json/WaterInfoReport.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/CleaningInfo.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/CleaningInfo.java new file mode 100644 index 0000000000000..a4dce40e367dc --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/CleaningInfo.java @@ -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 areaDefinition; + + CleanStateInfo(CleanMode mode) { + this(mode, Optional.empty()); + } + + CleanStateInfo(CleanMode mode, Optional 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 pointOpt = XPathUtils.getFirstXPathMatchOpt(xml, "//clean/@p"); + if (pointOpt.isPresent()) { + return new CleanStateInfo(CleanMode.CUSTOM_AREA, pointOpt.map(n -> n.getNodeValue())); + } + Optional 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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/DeviceInfo.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/DeviceInfo.java new file mode 100644 index 0000000000000..9ac668558bbe7 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/DeviceInfo.java @@ -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 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 parseErrorInfo(String xml) throws DataParsingException { + for (String attr : ERROR_ATTR_NAMES) { + Optional 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 value = nodeValueToInt(xml, "value"); + Optional total = nodeValueToInt(xml, "total"); + Optional 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 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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/WaterSystemInfo.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/WaterSystemInfo.java new file mode 100644 index 0000000000000..2116b338fefa6 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/deviceapi/xml/WaterSystemInfo.java @@ -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); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/AccessData.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/AccessData.java new file mode 100644 index 0000000000000..9e3bec464390e --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/AccessData.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/AuthCode.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/AuthCode.java new file mode 100644 index 0000000000000..506a4f424b231 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/AuthCode.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/ResponseWrapper.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/ResponseWrapper.java new file mode 100644 index 0000000000000..671206262e5dd --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/main/ResponseWrapper.java @@ -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 { + @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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/AbstractPortalIotCommandResponse.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/AbstractPortalIotCommandResponse.java new file mode 100644 index 0000000000000..339c88c3c139c --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/AbstractPortalIotCommandResponse.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/AbstractPortalResponse.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/AbstractPortalResponse.java new file mode 100644 index 0000000000000..693655ef4fbff --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/AbstractPortalResponse.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/Device.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/Device.java new file mode 100644 index 0000000000000..44f622d24bbf7 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/Device.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/IotProduct.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/IotProduct.java new file mode 100644 index 0000000000000..cd0f4315514cb --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/IotProduct.java @@ -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; + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanLogsResponse.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanLogsResponse.java new file mode 100644 index 0000000000000..93d5dafdc94b2 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalCleanLogsResponse.java @@ -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 records; + + @SerializedName("ret") + final String result; + + PortalCleanLogsResponse(String result, List records) { + this.result = result; + this.records = records; + } + + public boolean wasSuccessful() { + return "ok".equals(result); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalDeviceResponse.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalDeviceResponse.java new file mode 100644 index 0000000000000..ac49dc2751707 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalDeviceResponse.java @@ -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 devices; + + public PortalDeviceResponse(String result, List devices) { + super(result); + this.devices = devices; + } + + public List getDevices() { + return devices; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotCommandJsonResponse.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotCommandJsonResponse.java new file mode 100644 index 0000000000000..29627a85108fc --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotCommandJsonResponse.java @@ -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 getResponsePayloadAs(Gson gson, Class 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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotCommandXmlResponse.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotCommandXmlResponse.java new file mode 100644 index 0000000000000..6fff6dc16a841 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotCommandXmlResponse.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotProductResponse.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotProductResponse.java new file mode 100644 index 0000000000000..6d45fdad31c01 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalIotProductResponse.java @@ -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 products; + + // unused field: 'code' (integer) + + public PortalIotProductResponse(List products) { + this.products = products; + } + + public List getProducts() { + return products; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalLoginResponse.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalLoginResponse.java new file mode 100644 index 0000000000000..26bed8dbb463d --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/PortalLoginResponse.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/Service.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/Service.java new file mode 100644 index 0000000000000..5fa694fdb1820 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/impl/dto/response/portal/Service.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/ChargeMode.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/ChargeMode.java new file mode 100644 index 0000000000000..c8230abf0d85e --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/ChargeMode.java @@ -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; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/CleanLogRecord.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/CleanLogRecord.java new file mode 100644 index 0000000000000..dc8df1d07bfbe --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/CleanLogRecord.java @@ -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 mapImageUrl; + public final CleanMode mode; + + public CleanLogRecord(long timestamp, long duration, int area, Optional mapImageUrl, CleanMode mode) { + this.timestamp = new Date(timestamp * 1000); + this.cleaningDuration = duration; + this.cleanedArea = area; + this.mapImageUrl = mapImageUrl; + this.mode = mode; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/CleanMode.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/CleanMode.java new file mode 100644 index 0000000000000..07817f6f05276 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/CleanMode.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/Component.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/Component.java new file mode 100644 index 0000000000000..56b9b64145f8c --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/Component.java @@ -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.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public enum Component { + BRUSH("Brush", "brush"), + SIDE_BRUSH("SideBrush", "sideBrush"), + DUST_CASE_HEAP("DustCaseHeap", "heap"), + UNIT_CARE("" /* not supported in XML */, "unitCare"); + + public final String xmlValue; + public final String jsonValue; + + private Component(String xmlValue, String jsonValue) { + this.xmlValue = xmlValue; + this.jsonValue = jsonValue; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/DeviceCapability.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/DeviceCapability.java new file mode 100644 index 0000000000000..d6dfea0ac5c95 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/DeviceCapability.java @@ -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.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public enum DeviceCapability { + @SerializedName("mopping_system") + MOPPING_SYSTEM, + @SerializedName("main_brush") + MAIN_BRUSH, + @SerializedName("voice_reporting") + VOICE_REPORTING, + @SerializedName("spot_area_cleaning") + SPOT_AREA_CLEANING, + @SerializedName("custom_area_cleaning") + CUSTOM_AREA_CLEANING, + @SerializedName("single_room_cleaning") + SINGLE_ROOM_CLEANING, + @SerializedName("clean_speed_control") + CLEAN_SPEED_CONTROL, + @SerializedName("mapping") + MAPPING, + @SerializedName("auto_empty_station") + AUTO_EMPTY_STATION, + @SerializedName("read_network_info") + READ_NETWORK_INFO, + @SerializedName("true_detect_3d") + TRUE_DETECT_3D, + @SerializedName("unit_care_lifespan") + UNIT_CARE_LIFESPAN, + // implicit capabilities added in code + EDGE_CLEANING, + SPOT_CLEANING, + EXTENDED_CLEAN_SPEED_CONTROL, + EXTENDED_CLEAN_LOG_RECORD, + DEFAULT_CLEAN_COUNT_SETTING +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/MoppingWaterAmount.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/MoppingWaterAmount.java new file mode 100644 index 0000000000000..07cdf60cea8a0 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/MoppingWaterAmount.java @@ -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.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public enum MoppingWaterAmount { + LOW, + MEDIUM, + HIGH, + VERY_HIGH; + + public static MoppingWaterAmount fromApiValue(int value) { + return MoppingWaterAmount.values()[value - 1]; + } + + public int toApiValue() { + return ordinal() + 1; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/NetworkInfo.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/NetworkInfo.java new file mode 100644 index 0000000000000..aa6ff69981a72 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/NetworkInfo.java @@ -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.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public class NetworkInfo { + public final String ipAddress; + public final String macAddress; + public final String wifiSsid; + public final int wifiRssi; + + public NetworkInfo(String ip, String mac, String ssid, int rssi) { + this.ipAddress = ip; + this.macAddress = mac; + this.wifiSsid = ssid; + this.wifiRssi = rssi; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/SpotAreaType.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/SpotAreaType.java new file mode 100644 index 0000000000000..c68bee50deda7 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/SpotAreaType.java @@ -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.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public enum SpotAreaType { + LIVING_ROOM(1), + DINING_ROOM(2), + BEDROOM(3), + OFFICE(4), + KITCHEN(5), + BATHROOM(6), + LAUNDRY_ROOM(7), + LOUNGE(8), + STORAGE_ROOM(9), + CHILDS_ROOM(10), + SUN_ROOM(11), + CORRIDOR(12), + BALCONY(13), + GYM(14); + + private final int type; + + private SpotAreaType(int type) { + this.type = type; + } + + public SpotAreaType fromApiResponse(String response) throws NumberFormatException, IllegalArgumentException { + int id = Integer.parseInt(response); + for (SpotAreaType t : values()) { + if (t.type == id) { + return t; + } + } + throw new IllegalArgumentException("Unknown spot area type " + response); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/SuctionPower.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/SuctionPower.java new file mode 100644 index 0000000000000..12992e1b866bb --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/model/SuctionPower.java @@ -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.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Johannes Ptaszyk - Initial contribution + */ +@NonNullByDefault +public enum SuctionPower { + @SerializedName("standard") + NORMAL, + @SerializedName("strong") + HIGH, + HIGHER, + SILENT; + + public static SuctionPower fromJsonValue(int value) { + switch (value) { + case 1000: + return SILENT; + case 1: + return HIGH; + case 2: + return HIGHER; + default: + return NORMAL; + } + } + + public int toJsonValue() { + switch (this) { + case HIGH: + return 1; + case HIGHER: + return 2; + case SILENT: + return 1000; + default: // NORMAL + return 0; + } + } + + public String toXmlValue() { + if (this == HIGH) { + return "strong"; + } + return "standard"; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/DataParsingException.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/DataParsingException.java new file mode 100644 index 0000000000000..c59faf6d98d22 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/DataParsingException.java @@ -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.util; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public class DataParsingException extends Exception { + private static final long serialVersionUID = -1486602104263772955L; + + public DataParsingException(String message) { + super(message); + } + + public DataParsingException(Exception cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/MD5Util.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/MD5Util.java new file mode 100644 index 0000000000000..0514c8b2222e5 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/MD5Util.java @@ -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.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Johannes Ptaszyk - Initial contribution + */ +@NonNullByDefault +public class MD5Util { + private static final Logger LOGGER = LoggerFactory.getLogger(MD5Util.class); + + private MD5Util() { + // Prevent instantiation of util class + } + + public static String getMD5Hash(String input) { + MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + LOGGER.error("Could not get MD5 MessageDigest instance", e); + return ""; + } + md.update(input.getBytes()); + byte[] hash = md.digest(); + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + if ((0xff & b) < 0x10) { + hexString.append("0").append(Integer.toHexString((0xFF & b))); + } else { + hexString.append(Integer.toHexString(0xFF & b)); + } + } + return hexString.toString(); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/SchedulerTask.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/SchedulerTask.java new file mode 100644 index 0000000000000..c0e8cd18d96de --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/SchedulerTask.java @@ -0,0 +1,97 @@ +/** + * 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.util; + +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; + +/** + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public class SchedulerTask implements Runnable { + private final Logger logger; + private final String name; + private String prefixedName; + private final Runnable runnable; + private final ScheduledExecutorService scheduler; + private @Nullable Future future; + + public SchedulerTask(ScheduledExecutorService scheduler, Logger logger, String name, Runnable runnable) { + this.logger = logger; + this.name = name; + this.prefixedName = name; + this.runnable = runnable; + this.scheduler = scheduler; + } + + public void setNamePrefix(String prefix) { + if (future != null) { + throw new IllegalStateException("Must not set prefix while scheduled"); + } + if (prefix.isEmpty()) { + prefixedName = name; + } else { + prefixedName = prefix + ": " + name; + } + } + + public void submit() { + schedule(0); + } + + public synchronized void schedule(long delaySeconds) { + if (future != null) { + logger.trace("{}: Already scheduled to run", prefixedName); + return; + } + logger.trace("{}: Scheduling to run in {} seconds", prefixedName, delaySeconds); + if (delaySeconds == 0) { + future = scheduler.submit(this); + } else { + future = scheduler.schedule(this, delaySeconds, TimeUnit.SECONDS); + } + } + + public synchronized void scheduleRecurring(long intervalSeconds) { + if (future != null) { + logger.trace("{}: Already scheduled to run", prefixedName); + return; + } + logger.trace("{}: Scheduling to run in {} second intervals", prefixedName, intervalSeconds); + future = scheduler.scheduleWithFixedDelay(runnable, 0, intervalSeconds, TimeUnit.SECONDS); + } + + public synchronized void cancel() { + Future future = this.future; + this.future = null; + if (future != null) { + future.cancel(true); + logger.trace("{}: Cancelled", prefixedName); + } + } + + @Override + public void run() { + synchronized (this) { + future = null; + } + logger.trace("{}: Running one-shot", prefixedName); + runnable.run(); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/XPathUtils.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/XPathUtils.java new file mode 100644 index 0000000000000..5305221272549 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/api/util/XPathUtils.java @@ -0,0 +1,67 @@ +/** + * 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.util; + +import java.io.StringReader; +import java.util.Optional; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +/** + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public class XPathUtils { + private static @Nullable XPathFactory factory; + + public static Node getFirstXPathMatch(String xml, String xpathExpression) throws DataParsingException { + NodeList nodes = getXPathMatches(xml, xpathExpression); + if (nodes.getLength() == 0) { + throw new DataParsingException("No nodes matching expression " + xpathExpression + " in XML " + xml); + } + return nodes.item(0); + } + + public static Optional getFirstXPathMatchOpt(String xml, String xpathExpression) throws DataParsingException { + NodeList nodes = getXPathMatches(xml, xpathExpression); + return nodes.getLength() == 0 ? Optional.empty() : Optional.of(nodes.item(0)); + } + + public static NodeList getXPathMatches(String xml, String xpathExpression) throws DataParsingException { + try { + InputSource source = new InputSource(new StringReader(xml)); + return (NodeList) newXPath().evaluate(xpathExpression, source, XPathConstants.NODESET); + } catch (XPathExpressionException e) { + throw new DataParsingException(e); + } + } + + @SuppressWarnings("null") // null annotations don't recognize FACTORY can not be null in return statement + private static XPath newXPath() { + synchronized (XPathUtils.class) { + if (factory == null) { + factory = XPathFactory.newInstance(); + } + return factory.newXPath(); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/config/EcovacsApiConfiguration.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/config/EcovacsApiConfiguration.java new file mode 100644 index 0000000000000..0e65c2decf69e --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/config/EcovacsApiConfiguration.java @@ -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.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EcovacsApiConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public class EcovacsApiConfiguration { + public String email = ""; + public String password = ""; + public String continent = "ww"; + public String installId = ""; +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/config/EcovacsVacuumConfiguration.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/config/EcovacsVacuumConfiguration.java new file mode 100644 index 0000000000000..11fbde9455e05 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/config/EcovacsVacuumConfiguration.java @@ -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.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EcovacsVacuumConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public class EcovacsVacuumConfiguration { + public String serialNumber = ""; + public int refresh = 5; // in minutes +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/discovery/EcovacsDeviceDiscoveryService.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/discovery/EcovacsDeviceDiscoveryService.java new file mode 100644 index 0000000000000..33a3ce49c0631 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/discovery/EcovacsDeviceDiscoveryService.java @@ -0,0 +1,151 @@ +/** + * 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.discovery; + +import static org.openhab.binding.ecovacs.internal.EcovacsBindingConstants.*; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.ecovacs.internal.api.EcovacsApi; +import org.openhab.binding.ecovacs.internal.api.EcovacsApiException; +import org.openhab.binding.ecovacs.internal.api.EcovacsDevice; +import org.openhab.binding.ecovacs.internal.api.util.SchedulerTask; +import org.openhab.binding.ecovacs.internal.handler.EcovacsApiHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EcovacsDeviceDiscoveryService} is used for discovering devices registered in the cloud account. + * + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +@Component(service = DiscoveryService.class, configurationPid = "discovery.ecovacs") +public class EcovacsDeviceDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + private final Logger logger = LoggerFactory.getLogger(EcovacsDeviceDiscoveryService.class); + + private static final int DISCOVER_TIMEOUT_SECONDS = 10; + + private @NonNullByDefault({}) EcovacsApiHandler apiHandler; + private Optional api = Optional.empty(); + private final SchedulerTask onDemandScanTask = new SchedulerTask(scheduler, logger, "OnDemandScan", + this::scanForDevices); + private final SchedulerTask backgroundScanTask = new SchedulerTask(scheduler, logger, "BackgroundScan", + this::scanForDevices); + + public EcovacsDeviceDiscoveryService() { + super(Collections.singleton(THING_TYPE_VACUUM), DISCOVER_TIMEOUT_SECONDS, true); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof EcovacsApiHandler) { + this.apiHandler = (EcovacsApiHandler) handler; + this.apiHandler.setDiscoveryService(this); + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return apiHandler; + } + + @Override + public void activate() { + super.activate(null); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + protected synchronized void startBackgroundDiscovery() { + stopBackgroundDiscovery(); + backgroundScanTask.scheduleRecurring(60); + } + + @Override + protected synchronized void stopBackgroundDiscovery() { + backgroundScanTask.cancel(); + } + + public synchronized void startScanningWithApi(EcovacsApi api) { + this.api = Optional.of(api); + onDemandScanTask.cancel(); + startScan(); + } + + @Override + public synchronized void startScan() { + logger.debug("Starting Ecovacs discovery scan"); + onDemandScanTask.submit(); + } + + @Override + public synchronized void stopScan() { + logger.debug("Stopping Ecovacs discovery scan"); + onDemandScanTask.cancel(); + super.stopScan(); + } + + private void scanForDevices() { + this.api.ifPresent(api -> { + long timestampOfLastScan = getTimestampOfLastScan(); + try { + List devices = api.getDevices(); + logger.debug("Ecovacs discovery found {} devices", devices.size()); + + for (EcovacsDevice device : devices) { + deviceDiscovered(device); + } + for (Thing thing : apiHandler.getThing().getThings()) { + String serial = thing.getUID().getId(); + if (!devices.stream().anyMatch(d -> serial.equals(d.getSerialNumber()))) { + thingRemoved(thing.getUID()); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (EcovacsApiException e) { + logger.debug("Could not retrieve devices from Ecovacs API", e); + } finally { + removeOlderResults(timestampOfLastScan); + } + }); + } + + private void deviceDiscovered(EcovacsDevice device) { + ThingUID thingUID = new ThingUID(THING_TYPE_VACUUM, apiHandler.getThing().getUID(), device.getSerialNumber()); + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID) + .withBridge(apiHandler.getThing().getUID()).withLabel(device.getModelName()) + .withProperty(Thing.PROPERTY_SERIAL_NUMBER, device.getSerialNumber()) + .withProperty(Thing.PROPERTY_MODEL_ID, device.getModelName()) + .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build(); + thingDiscovered(discoveryResult); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/handler/EcovacsApiHandler.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/handler/EcovacsApiHandler.java new file mode 100644 index 0000000000000..70c26224ae72e --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/handler/EcovacsApiHandler.java @@ -0,0 +1,153 @@ +/** + * 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.handler; + +import static org.openhab.binding.ecovacs.internal.EcovacsBindingConstants.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.UUID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.ecovacs.internal.api.EcovacsApi; +import org.openhab.binding.ecovacs.internal.api.EcovacsApiException; +import org.openhab.binding.ecovacs.internal.api.util.SchedulerTask; +import org.openhab.binding.ecovacs.internal.config.EcovacsApiConfiguration; +import org.openhab.binding.ecovacs.internal.discovery.EcovacsDeviceDiscoveryService; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.ConfigurationException; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EcovacsApiHandler} is responsible for connecting to the Ecovacs cloud API account. + * + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public class EcovacsApiHandler extends BaseBridgeHandler { + private final Logger logger = LoggerFactory.getLogger(EcovacsApiHandler.class); + private static final long RETRY_INTERVAL_SECONDS = 120; + + private Optional discoveryService = Optional.empty(); + private SchedulerTask loginTask; + private final HttpClient httpClient; + private final LocaleProvider localeProvider; + + public EcovacsApiHandler(Bridge bridge, HttpClient httpClient, LocaleProvider localeProvider) { + super(bridge); + this.httpClient = httpClient; + this.localeProvider = localeProvider; + this.loginTask = new SchedulerTask(scheduler, logger, "API Login", this::loginToApi); + } + + public void setDiscoveryService(EcovacsDeviceDiscoveryService discoveryService) { + this.discoveryService = Optional.of(discoveryService); + } + + public EcovacsApi createApiForDevice(String serial) throws ConfigurationException { + String country = localeProvider.getLocale().getCountry(); + if (country.isEmpty()) { + throw new ConfigurationException("@text/offline.config-error-no-country"); + } + return createApi("-" + serial, country); + } + + @Override + public void initialize() { + logger.debug("Initializing Ecovacs account '{}'", getThing().getUID().getId()); + // The API expects us to provide a unique device ID during authentication, so generate one once + // and keep it in configuration afterwards + if (!getConfig().keySet().contains("installId")) { + Configuration newConfig = editConfiguration(); + newConfig.put("installId", UUID.randomUUID().toString()); + updateConfiguration(newConfig); + } + updateStatus(ThingStatus.UNKNOWN); + loginTask.submit(); + } + + @Override + public void dispose() { + super.dispose(); + discoveryService.ifPresent(ds -> ds.stopScan()); + } + + @Override + public Collection> getServices() { + return Collections.singleton(EcovacsDeviceDiscoveryService.class); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (RefreshType.REFRESH == command) { + logger.debug("Refreshing Ecovacs API account '{}'", getThing().getUID().getId()); + scheduleLogin(0); + } + } + + public void onLoginExpired() { + logger.debug("Ecovacs API login for account '{}' expired, logging in again", getThing().getUID().getId()); + scheduleLogin(0); + } + + private void scheduleLogin(long delaySeconds) { + loginTask.cancel(); + loginTask.schedule(delaySeconds); + } + + private EcovacsApi createApi(String deviceIdSuffix, String country) { + EcovacsApiConfiguration config = getConfigAs(EcovacsApiConfiguration.class); + String deviceId = config.installId + deviceIdSuffix; + org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration apiConfig = new org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration( + deviceId, config.email, config.password, config.continent, country, "EN", CLIENT_KEY, CLIENT_SECRET, + AUTH_CLIENT_KEY, AUTH_CLIENT_SECRET); + + return EcovacsApi.create(httpClient, apiConfig); + } + + private void loginToApi() { + try { + String country = localeProvider.getLocale().getCountry(); + if (country.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.config-error-no-country"); + return; + } + EcovacsApi api = createApi("", country); + api.loginAndGetAccessToken(); + updateStatus(ThingStatus.ONLINE); + discoveryService.ifPresent(ds -> ds.startScanningWithApi(api)); + + logger.debug("Ecovacs API initialized"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + updateStatus(ThingStatus.OFFLINE); + } catch (EcovacsApiException e) { + logger.debug("Ecovacs API login failed", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + scheduleLogin(RETRY_INTERVAL_SECONDS); + } + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/handler/EcovacsVacuumHandler.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/handler/EcovacsVacuumHandler.java new file mode 100644 index 0000000000000..febdb7edc17ef --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/handler/EcovacsVacuumHandler.java @@ -0,0 +1,822 @@ +/** + * 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.handler; + +import static org.openhab.binding.ecovacs.internal.EcovacsBindingConstants.*; + +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.ecovacs.internal.EcovacsDynamicStateDescriptionProvider; +import org.openhab.binding.ecovacs.internal.action.EcovacsVacuumActions; +import org.openhab.binding.ecovacs.internal.api.EcovacsApi; +import org.openhab.binding.ecovacs.internal.api.EcovacsApiException; +import org.openhab.binding.ecovacs.internal.api.EcovacsDevice; +import org.openhab.binding.ecovacs.internal.api.commands.AbstractNoResponseCommand; +import org.openhab.binding.ecovacs.internal.api.commands.CustomAreaCleaningCommand; +import org.openhab.binding.ecovacs.internal.api.commands.EmptyDustbinCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetBatteryInfoCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetChargeStateCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetCleanStateCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetComponentLifeSpanCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetContinuousCleaningCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetDefaultCleanPassesCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetDustbinAutoEmptyCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetErrorCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetMoppingWaterAmountCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetNetworkInfoCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetSuctionPowerCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetTotalStatsCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetTotalStatsCommand.TotalStats; +import org.openhab.binding.ecovacs.internal.api.commands.GetTrueDetectCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetVolumeCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GetWaterSystemPresentCommand; +import org.openhab.binding.ecovacs.internal.api.commands.GoChargingCommand; +import org.openhab.binding.ecovacs.internal.api.commands.PauseCleaningCommand; +import org.openhab.binding.ecovacs.internal.api.commands.PlaySoundCommand; +import org.openhab.binding.ecovacs.internal.api.commands.ResumeCleaningCommand; +import org.openhab.binding.ecovacs.internal.api.commands.SetContinuousCleaningCommand; +import org.openhab.binding.ecovacs.internal.api.commands.SetDefaultCleanPassesCommand; +import org.openhab.binding.ecovacs.internal.api.commands.SetDustbinAutoEmptyCommand; +import org.openhab.binding.ecovacs.internal.api.commands.SetMoppingWaterAmountCommand; +import org.openhab.binding.ecovacs.internal.api.commands.SetSuctionPowerCommand; +import org.openhab.binding.ecovacs.internal.api.commands.SetTrueDetectCommand; +import org.openhab.binding.ecovacs.internal.api.commands.SetVolumeCommand; +import org.openhab.binding.ecovacs.internal.api.commands.SpotAreaCleaningCommand; +import org.openhab.binding.ecovacs.internal.api.commands.StartAutoCleaningCommand; +import org.openhab.binding.ecovacs.internal.api.commands.StopCleaningCommand; +import org.openhab.binding.ecovacs.internal.api.model.ChargeMode; +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.Component; +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.NetworkInfo; +import org.openhab.binding.ecovacs.internal.api.model.SuctionPower; +import org.openhab.binding.ecovacs.internal.api.util.SchedulerTask; +import org.openhab.binding.ecovacs.internal.config.EcovacsVacuumConfiguration; +import org.openhab.binding.ecovacs.internal.util.StateOptionEntry; +import org.openhab.binding.ecovacs.internal.util.StateOptionMapping; +import org.openhab.core.i18n.ConfigurationException; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.io.net.http.HttpUtil; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.StateOption; +import org.openhab.core.types.UnDefType; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EcovacsVacuumHandler} is responsible for handling data and commands from/to vacuum cleaners. + * + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDevice.EventListener { + + private final Logger logger = LoggerFactory.getLogger(EcovacsVacuumHandler.class); + + private final TranslationProvider i18Provider; + private final LocaleProvider localeProvider; + private final EcovacsDynamicStateDescriptionProvider stateDescriptionProvider; + private final Bundle bundle; + + private final SchedulerTask initTask; + private final SchedulerTask reconnectTask; + private final SchedulerTask pollTask; + private @Nullable EcovacsDevice device; + + private @Nullable Boolean lastWasCharging; + private @Nullable CleanMode lastCleanMode; + private @Nullable CleanMode lastActiveCleanMode; + private Optional lastDownloadedCleanMapUrl = Optional.empty(); + private long lastSuccessfulPollTimestamp; + private int lastDefaultCleaningPasses = 1; + private String serialNumber = ""; + + public EcovacsVacuumHandler(Thing thing, TranslationProvider i18Provider, LocaleProvider localeProvider, + EcovacsDynamicStateDescriptionProvider stateDescriptionProvider) { + super(thing); + this.i18Provider = i18Provider; + this.localeProvider = localeProvider; + this.stateDescriptionProvider = stateDescriptionProvider; + bundle = FrameworkUtil.getBundle(getClass()); + + initTask = new SchedulerTask(scheduler, logger, "Init", this::initDevice); + reconnectTask = new SchedulerTask(scheduler, logger, "Connection", this::connectToDevice); + pollTask = new SchedulerTask(scheduler, logger, "Poll", this::pollData); + } + + @Override + public Collection> getServices() { + return Collections.singleton(EcovacsVacuumActions.class); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + final EcovacsDevice device = this.device; + if (device == null) { + logger.debug("{}: Ignoring command {}, no active connection", serialNumber, command); + return; + } + String channel = channelUID.getId(); + + try { + if (channel.equals(CHANNEL_ID_COMMAND) && command instanceof StringType) { + AbstractNoResponseCommand cmd = determineDeviceCommand(device, command.toString()); + if (cmd != null) { + device.sendCommand(cmd); + return; + } + } else if (channel.equals(CHANNEL_ID_VOICE_VOLUME) && command instanceof DecimalType) { + int volumePercent = ((DecimalType) command).intValue(); + device.sendCommand(new SetVolumeCommand((volumePercent + 5) / 10)); + return; + } else if (channel.equals(CHANNEL_ID_SUCTION_POWER) && command instanceof StringType) { + Optional power = SUCTION_POWER_MAPPING.findMappedEnumValue(command.toString()); + if (power.isPresent()) { + device.sendCommand(new SetSuctionPowerCommand(power.get())); + return; + } + } else if (channel.equals(CHANNEL_ID_WATER_AMOUNT) && command instanceof StringType) { + Optional amount = WATER_AMOUNT_MAPPING.findMappedEnumValue(command.toString()); + if (amount.isPresent()) { + device.sendCommand(new SetMoppingWaterAmountCommand(amount.get())); + return; + } + } else if (channel.equals(CHANNEL_ID_AUTO_EMPTY)) { + if (command instanceof OnOffType) { + device.sendCommand(new SetDustbinAutoEmptyCommand(command == OnOffType.ON)); + return; + } else if (command instanceof StringType && command.toString().equals("trigger")) { + device.sendCommand(new EmptyDustbinCommand()); + return; + } + } else if (channel.equals(CHANNEL_ID_TRUE_DETECT_3D) && command instanceof OnOffType) { + device.sendCommand(new SetTrueDetectCommand(command == OnOffType.ON)); + return; + } else if (channel.equals(CHANNEL_ID_CONTINUOUS_CLEANING) && command instanceof OnOffType) { + device.sendCommand(new SetContinuousCleaningCommand(command == OnOffType.ON)); + return; + } else if (channel.equals(CHANNEL_ID_CLEANING_PASSES) && command instanceof DecimalType) { + int passes = ((DecimalType) command).intValue(); + device.sendCommand(new SetDefaultCleanPassesCommand(passes)); + lastDefaultCleaningPasses = passes; // if we get here, the command was executed successfully + return; + } + logger.debug("{}: Ignoring unsupported device command {} for channel {}", serialNumber, command, channel); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (EcovacsApiException e) { + logger.debug("{}: Handling device command {} failed", serialNumber, command, e); + } + } + + @Override + public void initialize() { + serialNumber = getConfigAs(EcovacsVacuumConfiguration.class).serialNumber; + if (serialNumber.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.config-error-no-serial"); + } else { + logger.debug("{}: Initializing handler", serialNumber); + updateStatus(ThingStatus.UNKNOWN); + initTask.setNamePrefix(serialNumber); + reconnectTask.setNamePrefix(serialNumber); + pollTask.setNamePrefix(serialNumber); + initTask.submit(); + } + } + + @Override + public void dispose() { + logger.debug("{}: Disposing handler", serialNumber); + teardown(false); + } + + @Override + public void channelLinked(ChannelUID channelUID) { + EcovacsDevice device = this.device; + if (device == null) { + return; + } + + try { + switch (channelUID.getId()) { + case CHANNEL_ID_BATTERY_LEVEL: + fetchInitialBatteryStatus(device); + break; + case CHANNEL_ID_STATE: + case CHANNEL_ID_COMMAND: + case CHANNEL_ID_CLEANING_MODE: + fetchInitialStateAndCommandValues(device); + break; + case CHANNEL_ID_WATER_PLATE_PRESENT: + fetchInitialWaterSystemPresentState(device); + break; + case CHANNEL_ID_ERROR_CODE: + case CHANNEL_ID_ERROR_DESCRIPTION: + fetchInitialErrorCode(device); + default: + scheduleNextPoll(5); // add some delay in case multiple channels are linked at once + break; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (EcovacsApiException e) { + logger.debug("{}: Fetching initial data for channel {} failed", serialNumber, channelUID.getId(), e); + } + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + logger.debug("{}: Bridge status changed to {}", serialNumber, bridgeStatusInfo); + if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) { + initTask.submit(); + } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) { + teardown(false); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + } + + @Override + public void onBatteryLevelUpdated(EcovacsDevice device, int newLevelPercent) { + // Some devices report weird values (> 100%), so better clamp to supported range + int actualPercent = Math.max(0, Math.min(newLevelPercent, 100)); + updateState(CHANNEL_ID_BATTERY_LEVEL, new DecimalType(actualPercent)); + } + + @Override + public void onChargingStateUpdated(EcovacsDevice device, boolean charging) { + lastWasCharging = charging; + updateStateAndCommandChannels(); + } + + @Override + public void onCleaningModeUpdated(EcovacsDevice device, CleanMode newMode, Optional areaDefinition) { + lastCleanMode = newMode; + if (newMode.isActive()) { + lastActiveCleanMode = newMode; + } else if (newMode.isIdle()) { + lastActiveCleanMode = null; + } + updateStateAndCommandChannels(); + Optional areaDefState = areaDefinition.map(def -> { + if (newMode == CleanMode.SPOT_AREA) { + // Map indices back to letters as shown in the app + def = Arrays.stream(def.split(",")).map(item -> { + try { + int index = Integer.parseInt(item); + return String.valueOf((char) ('A' + index)); + } catch (NumberFormatException e) { + return item; + } + }).collect(Collectors.joining(";")); + } else if (newMode == CleanMode.CUSTOM_AREA) { + // Map the separator from comma to semicolon to allow using the output as command input + def = def.replace(',', ';'); + } + return new StringType(def); + }); + updateState(CHANNEL_ID_CLEANING_SPOT_DEFINITION, areaDefState.orElse(UnDefType.UNDEF)); + if (newMode == CleanMode.RETURNING) { + scheduleNextPoll(30); + } else if (newMode.isIdle()) { + updateState(CHANNEL_ID_CLEANED_AREA, UnDefType.UNDEF); + updateState(CHANNEL_ID_CLEANING_TIME, UnDefType.UNDEF); + } + } + + @Override + public void onCleaningStatsUpdated(EcovacsDevice device, int cleanedArea, int cleaningTimeSeconds) { + updateState(CHANNEL_ID_CLEANED_AREA, new QuantityType<>(cleanedArea, SIUnits.SQUARE_METRE)); + updateState(CHANNEL_ID_CLEANING_TIME, new QuantityType<>(cleaningTimeSeconds, Units.SECOND)); + } + + @Override + public void onWaterSystemPresentUpdated(EcovacsDevice device, boolean present) { + updateState(CHANNEL_ID_WATER_PLATE_PRESENT, OnOffType.from(present)); + } + + @Override + public void onErrorReported(EcovacsDevice device, int errorCode) { + updateState(CHANNEL_ID_ERROR_CODE, new DecimalType(errorCode)); + final Locale locale = localeProvider.getLocale(); + String errorDesc = i18Provider.getText(bundle, "ecovacs.vacuum.error-code." + errorCode, null, locale); + if (errorDesc == null) { + errorDesc = i18Provider.getText(bundle, "ecovacs.vacuum.error-code.unknown", "", locale, errorCode); + } + updateState(CHANNEL_ID_ERROR_DESCRIPTION, new StringType(errorDesc)); + } + + @Override + public void onEventStreamFailure(final EcovacsDevice device, Throwable error) { + logger.debug("{}: Device connection failed, reconnecting", serialNumber, error); + teardownAndScheduleReconnection(); + } + + @Override + public void onFirmwareVersionChanged(EcovacsDevice device, String fwVersion) { + updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, fwVersion); + } + + public void playSound(PlaySoundCommand command) { + doWithDevice(device -> { + if (device.hasCapability(DeviceCapability.VOICE_REPORTING)) { + device.sendCommand(command); + } else { + logger.info("{}: Device does not support voice reporting, ignoring sound action", serialNumber); + } + }); + } + + private void fetchInitialBatteryStatus(EcovacsDevice device) throws EcovacsApiException, InterruptedException { + Integer batteryPercent = device.sendCommand(new GetBatteryInfoCommand()); + onBatteryLevelUpdated(device, batteryPercent); + } + + private void fetchInitialStateAndCommandValues(EcovacsDevice device) + throws EcovacsApiException, InterruptedException { + lastWasCharging = device.sendCommand(new GetChargeStateCommand()) == ChargeMode.CHARGING; + CleanMode mode = device.sendCommand(new GetCleanStateCommand()); + if (mode.isActive()) { + lastActiveCleanMode = mode; + } + lastCleanMode = mode; + updateStateAndCommandChannels(); + } + + private void fetchInitialWaterSystemPresentState(EcovacsDevice device) + throws EcovacsApiException, InterruptedException { + if (!device.hasCapability(DeviceCapability.MOPPING_SYSTEM)) { + return; + } + boolean present = device.sendCommand(new GetWaterSystemPresentCommand()); + onWaterSystemPresentUpdated(device, present); + } + + private void fetchInitialErrorCode(EcovacsDevice device) throws EcovacsApiException, InterruptedException { + Optional errorOpt = device.sendCommand(new GetErrorCommand()); + if (errorOpt.isPresent()) { + onErrorReported(device, errorOpt.get()); + } + } + + private void removeUnsupportedChannels(EcovacsDevice device) { + ThingBuilder builder = editThing(); + boolean hasChanges = false; + + if (!device.hasCapability(DeviceCapability.MOPPING_SYSTEM)) { + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_WATER_AMOUNT); + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_WATER_PLATE_PRESENT); + } + if (!device.hasCapability(DeviceCapability.CLEAN_SPEED_CONTROL)) { + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_SUCTION_POWER); + } + if (!device.hasCapability(DeviceCapability.MAIN_BRUSH)) { + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_MAIN_BRUSH_LIFETIME); + } + if (!device.hasCapability(DeviceCapability.VOICE_REPORTING)) { + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_VOICE_VOLUME); + } + if (!device.hasCapability(DeviceCapability.EXTENDED_CLEAN_LOG_RECORD)) { + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_LAST_CLEAN_MODE); + } + if (!device.hasCapability(DeviceCapability.MAPPING) + || !device.hasCapability(DeviceCapability.EXTENDED_CLEAN_LOG_RECORD)) { + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_LAST_CLEAN_MAP); + } + if (!device.hasCapability(DeviceCapability.READ_NETWORK_INFO)) { + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_WIFI_RSSI); + } + if (!device.hasCapability(DeviceCapability.AUTO_EMPTY_STATION)) { + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_AUTO_EMPTY); + } + if (!device.hasCapability(DeviceCapability.TRUE_DETECT_3D)) { + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_TRUE_DETECT_3D); + } + if (!device.hasCapability(DeviceCapability.DEFAULT_CLEAN_COUNT_SETTING)) { + hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_CLEANING_PASSES); + } + + if (hasChanges) { + updateThing(builder.build()); + } + } + + private boolean removeUnsupportedChannel(ThingBuilder builder, String channelId) { + ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelId); + if (getThing().getChannel(channelUID) == null) { + return false; + } + logger.debug("{}: Removing unsupported channel {}", serialNumber, channelId); + builder.withoutChannel(channelUID); + return true; + } + + private void updateStateOptions(EcovacsDevice device) { + List modeChannelOptions = createChannelOptions(device, CleanMode.values(), CLEAN_MODE_MAPPING, + m -> m.enumValue.isActive()); + ThingUID thingUID = getThing().getUID(); + + stateDescriptionProvider.setStateOptions(new ChannelUID(thingUID, CHANNEL_ID_CLEANING_MODE), + modeChannelOptions); + stateDescriptionProvider.setStateOptions(new ChannelUID(thingUID, CHANNEL_ID_LAST_CLEAN_MODE), + modeChannelOptions); + stateDescriptionProvider.setStateOptions(new ChannelUID(thingUID, CHANNEL_ID_SUCTION_POWER), + createChannelOptions(device, SuctionPower.values(), SUCTION_POWER_MAPPING, null)); + stateDescriptionProvider.setStateOptions(new ChannelUID(thingUID, CHANNEL_ID_WATER_AMOUNT), + createChannelOptions(device, MoppingWaterAmount.values(), WATER_AMOUNT_MAPPING, null)); + } + + private > List createChannelOptions(EcovacsDevice device, T[] values, + StateOptionMapping mapping, @Nullable Predicate> filter) { + return Arrays.stream(values).map(v -> Optional.ofNullable(mapping.get(v))) + // ensure we have a mapping (should always be the case) + .filter(Optional::isPresent).map(opt -> opt.get()) + // apply supplied filter + .filter(mv -> filter == null || filter.test(mv)) + // apply capability filter + .filter(mv -> mv.capability.isEmpty() || device.hasCapability(mv.capability.get())) + // map to actual option + .map(mv -> new StateOption(mv.value, mv.value)).collect(Collectors.toList()); + } + + private synchronized void scheduleNextPoll(long initialDelaySeconds) { + final EcovacsVacuumConfiguration config = getConfigAs(EcovacsVacuumConfiguration.class); + final long delayUntilNextPoll; + if (initialDelaySeconds < 0) { + long intervalSeconds = config.refresh * 60; + long secondsSinceLastPoll = (System.currentTimeMillis() - lastSuccessfulPollTimestamp) / 1000; + long deltaRemaining = intervalSeconds - secondsSinceLastPoll; + delayUntilNextPoll = Math.max(0, deltaRemaining); + } else { + delayUntilNextPoll = initialDelaySeconds; + } + logger.debug("{}: Scheduling next poll in {}s, refresh interval {}min", serialNumber, delayUntilNextPoll, + config.refresh); + pollTask.cancel(); + pollTask.schedule(delayUntilNextPoll); + } + + private void initDevice() { + final EcovacsApiHandler handler = getApiHandler(); + if (handler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED); + return; + } + + try { + final EcovacsApi api = handler.createApiForDevice(serialNumber); + api.loginAndGetAccessToken(); + Optional deviceOpt = api.getDevices().stream() + .filter(d -> serialNumber.equals(d.getSerialNumber())).findFirst(); + if (deviceOpt.isPresent()) { + EcovacsDevice device = deviceOpt.get(); + this.device = device; + updateProperty(Thing.PROPERTY_MODEL_ID, device.getModelName()); + updateProperty(Thing.PROPERTY_SERIAL_NUMBER, device.getSerialNumber()); + updateStateOptions(device); + removeUnsupportedChannels(device); + connectToDevice(); + } else { + logger.info("{}: Device not found in device list, setting offline", serialNumber); + updateStatus(ThingStatus.OFFLINE); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ConfigurationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getRawMessage()); + } catch (EcovacsApiException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + private void teardownAndScheduleReconnection() { + teardown(true); + } + + private synchronized void teardown(boolean scheduleReconnection) { + EcovacsDevice device = this.device; + if (device != null) { + device.disconnect(scheduler); + } + + pollTask.cancel(); + + reconnectTask.cancel(); + initTask.cancel(); + + if (scheduleReconnection) { + SchedulerTask connectTask = device != null ? reconnectTask : initTask; + connectTask.schedule(5); + } + } + + private void connectToDevice() { + doWithDevice(device -> { + device.connect(this, scheduler); + fetchInitialBatteryStatus(device); + fetchInitialStateAndCommandValues(device); + fetchInitialWaterSystemPresentState(device); // nop if unsupported + fetchInitialErrorCode(device); + scheduleNextPoll(-1); + logger.debug("{}: Device connected", serialNumber); + updateStatus(ThingStatus.ONLINE); + }); + } + + private void pollData() { + logger.debug("{}: Polling data", serialNumber); + doWithDevice(device -> { + TotalStats totalStats = device.sendCommand(new GetTotalStatsCommand()); + updateState(CHANNEL_ID_TOTAL_CLEANED_AREA, new QuantityType<>(totalStats.totalArea, SIUnits.SQUARE_METRE)); + updateState(CHANNEL_ID_TOTAL_CLEANING_TIME, new QuantityType<>(totalStats.totalRuntime, Units.SECOND)); + updateState(CHANNEL_ID_TOTAL_CLEAN_RUNS, new DecimalType(totalStats.cleanRuns)); + + boolean continuousCleaningEnabled = device.sendCommand(new GetContinuousCleaningCommand()); + updateState(CHANNEL_ID_CONTINUOUS_CLEANING, continuousCleaningEnabled ? OnOffType.ON : OnOffType.OFF); + + List cleanLogRecords = device.getCleanLogs(); + if (!cleanLogRecords.isEmpty()) { + CleanLogRecord record = cleanLogRecords.get(0); + + updateState(CHANNEL_ID_LAST_CLEAN_START, + new DateTimeType(record.timestamp.toInstant().atZone(ZoneId.systemDefault()))); + updateState(CHANNEL_ID_LAST_CLEAN_DURATION, new QuantityType<>(record.cleaningDuration, Units.SECOND)); + updateState(CHANNEL_ID_LAST_CLEAN_AREA, new QuantityType<>(record.cleanedArea, SIUnits.SQUARE_METRE)); + if (device.hasCapability(DeviceCapability.EXTENDED_CLEAN_LOG_RECORD)) { + StateOptionEntry mode = CLEAN_MODE_MAPPING.get(record.mode); + updateState(CHANNEL_ID_LAST_CLEAN_MODE, stringToState(mode != null ? mode.value : null)); + + if (device.hasCapability(DeviceCapability.MAPPING) + && !lastDownloadedCleanMapUrl.equals(record.mapImageUrl)) { + updateState(CHANNEL_ID_LAST_CLEAN_MAP, record.mapImageUrl.flatMap(url -> { + // HttpUtil expects the server to return the correct MIME type, but Ecovacs' server sends + // 'application/octet-stream', so we have to set the correct MIME type by ourselves + @Nullable + RawType mapData = HttpUtil.downloadData(url, null, false, -1); + if (mapData != null) { + mapData = new RawType(mapData.getBytes(), "image/png"); + lastDownloadedCleanMapUrl = record.mapImageUrl; + } else { + logger.debug("{}: Downloading cleaning map {} failed", serialNumber, url); + } + return Optional.ofNullable((State) mapData); + }).orElse(UnDefType.NULL)); + } + } + } + + if (device.hasCapability(DeviceCapability.CLEAN_SPEED_CONTROL)) { + SuctionPower power = device.sendCommand(new GetSuctionPowerCommand()); + updateState(CHANNEL_ID_SUCTION_POWER, new StringType(SUCTION_POWER_MAPPING.getMappedValue(power))); + } + + if (device.hasCapability(DeviceCapability.MOPPING_SYSTEM)) { + MoppingWaterAmount waterAmount = device.sendCommand(new GetMoppingWaterAmountCommand()); + updateState(CHANNEL_ID_WATER_AMOUNT, new StringType(WATER_AMOUNT_MAPPING.getMappedValue(waterAmount))); + } + + if (device.hasCapability(DeviceCapability.READ_NETWORK_INFO)) { + NetworkInfo netInfo = device.sendCommand(new GetNetworkInfoCommand()); + if (netInfo.wifiRssi != 0) { + updateState(CHANNEL_ID_WIFI_RSSI, new QuantityType<>(netInfo.wifiRssi, Units.DECIBEL_MILLIWATTS)); + } + } + + if (device.hasCapability(DeviceCapability.AUTO_EMPTY_STATION)) { + boolean autoEmptyEnabled = device.sendCommand(new GetDustbinAutoEmptyCommand()); + updateState(CHANNEL_ID_AUTO_EMPTY, autoEmptyEnabled ? OnOffType.ON : OnOffType.OFF); + } + if (device.hasCapability(DeviceCapability.TRUE_DETECT_3D)) { + boolean trueDetectEnabled = device.sendCommand(new GetTrueDetectCommand()); + updateState(CHANNEL_ID_TRUE_DETECT_3D, trueDetectEnabled ? OnOffType.ON : OnOffType.OFF); + } + if (device.hasCapability(DeviceCapability.DEFAULT_CLEAN_COUNT_SETTING)) { + lastDefaultCleaningPasses = device.sendCommand(new GetDefaultCleanPassesCommand()); + updateState(CHANNEL_ID_CLEANING_PASSES, new DecimalType(lastDefaultCleaningPasses)); + } + + int sideBrushPercent = device.sendCommand(new GetComponentLifeSpanCommand(Component.SIDE_BRUSH)); + updateState(CHANNEL_ID_SIDE_BRUSH_LIFETIME, new QuantityType<>(sideBrushPercent, Units.PERCENT)); + int filterPercent = device.sendCommand(new GetComponentLifeSpanCommand(Component.DUST_CASE_HEAP)); + updateState(CHANNEL_ID_DUST_FILTER_LIFETIME, new QuantityType<>(filterPercent, Units.PERCENT)); + + if (device.hasCapability(DeviceCapability.MAIN_BRUSH)) { + int mainBrushPercent = device.sendCommand(new GetComponentLifeSpanCommand(Component.BRUSH)); + updateState(CHANNEL_ID_MAIN_BRUSH_LIFETIME, new QuantityType<>(mainBrushPercent, Units.PERCENT)); + } + if (device.hasCapability(DeviceCapability.UNIT_CARE_LIFESPAN)) { + int unitCarePercent = device.sendCommand(new GetComponentLifeSpanCommand(Component.UNIT_CARE)); + updateState(CHANNEL_ID_OTHER_COMPONENT_LIFETIME, new QuantityType<>(unitCarePercent, Units.PERCENT)); + } + if (device.hasCapability(DeviceCapability.VOICE_REPORTING)) { + int level = device.sendCommand(new GetVolumeCommand()); + updateState(CHANNEL_ID_VOICE_VOLUME, new PercentType(level * 10)); + } + + lastSuccessfulPollTimestamp = System.currentTimeMillis(); + scheduleNextPoll(-1); + }); + logger.debug("{}: Data polling completed", serialNumber); + } + + private void updateStateAndCommandChannels() { + Boolean charging = this.lastWasCharging; + CleanMode cleanMode = this.lastCleanMode; + if (charging == null || cleanMode == null) { + return; + } + String commandState = determineCommandChannelValue(charging, cleanMode); + String currentMode = determineCleaningModeChannelValue(cleanMode.isActive() ? cleanMode : lastActiveCleanMode); + updateState(CHANNEL_ID_STATE, StringType.valueOf(determineStateChannelValue(charging, cleanMode))); + updateState(CHANNEL_ID_CLEANING_MODE, stringToState(currentMode)); + updateState(CHANNEL_ID_COMMAND, stringToState(commandState)); + } + + private String determineStateChannelValue(boolean charging, CleanMode cleanMode) { + if (charging) { + // Some devices already report charging state while returning to charging station, make sure to not report + // charging in that case. The same applies for models with pad washing/drying station, as those states imply + // the device being charging. + if (cleanMode != CleanMode.RETURNING && cleanMode != CleanMode.WASHING && cleanMode != CleanMode.DRYING) { + return "charging"; + } + } + if (cleanMode.isActive()) { + return "cleaning"; + } + StateOptionEntry result = CLEAN_MODE_MAPPING.get(cleanMode); + return result != null ? result.value : "idle"; + } + + private @Nullable String determineCleaningModeChannelValue(@Nullable CleanMode activeCleanMode) { + StateOptionEntry result = activeCleanMode != null ? CLEAN_MODE_MAPPING.get(activeCleanMode) : null; + return result != null ? result.value : null; + } + + private @Nullable String determineCommandChannelValue(boolean charging, CleanMode cleanMode) { + if (charging) { + return CMD_CHARGE; + } + switch (cleanMode) { + case AUTO: + return CMD_AUTO_CLEAN; + case SPOT_AREA: + return CMD_SPOT_AREA; + case PAUSE: + return CMD_PAUSE; + case STOP: + return CMD_STOP; + case RETURNING: + return CMD_CHARGE; + default: + break; + } + return null; + } + + private State stringToState(@Nullable String value) { + Optional stateOpt = Optional.ofNullable(value).map(v -> StringType.valueOf(v)); + return stateOpt.orElse(UnDefType.UNDEF); + } + + private @Nullable AbstractNoResponseCommand determineDeviceCommand(EcovacsDevice device, String command) { + CleanMode mode = lastActiveCleanMode; + + switch (command) { + case CMD_AUTO_CLEAN: + return new StartAutoCleaningCommand(); + case CMD_PAUSE: + if (mode != null) { + return new PauseCleaningCommand(mode); + } + break; + case CMD_RESUME: + if (mode != null) { + return new ResumeCleaningCommand(mode); + } + break; + case CMD_STOP: + return new StopCleaningCommand(); + case CMD_CHARGE: + return new GoChargingCommand(); + } + + if (command.startsWith(CMD_SPOT_AREA) && device.hasCapability(DeviceCapability.SPOT_AREA_CLEANING)) { + String[] splitted = command.split(":"); + if (splitted.length == 2 || splitted.length == 3) { + int passes = splitted.length == 3 && "x2".equals(splitted[2]) ? 2 : lastDefaultCleaningPasses; + List roomIds = new ArrayList<>(); + for (String id : splitted[1].split(";")) { + // We let the user pass in letters as in Ecovacs' app, but the API wants indices + if (id.length() == 1 && id.charAt(0) >= 'A' && id.charAt(0) <= 'Z') { + roomIds.add(String.valueOf(id.charAt(0) - 'A')); + } else { + logger.info("{}: Found invalid spot area room ID '{}', ignoring.", serialNumber, id); + } + } + if (!roomIds.isEmpty()) { + return new SpotAreaCleaningCommand(roomIds, passes); + } + } else { + logger.info("{}: spotArea command needs to have the form spotArea:[;][;<...roomX>][:x2]", + serialNumber); + } + } + if (command.startsWith(CMD_CUSTOM_AREA) && device.hasCapability(DeviceCapability.CUSTOM_AREA_CLEANING)) { + String[] splitted = command.split(":"); + if (splitted.length == 2 || splitted.length == 3) { + String coords = splitted[1]; + int passes = splitted.length == 3 && "x2".equals(splitted[2]) ? 2 : lastDefaultCleaningPasses; + String[] splittedAreaDef = coords.split(";"); + if (splittedAreaDef.length == 4) { + return new CustomAreaCleaningCommand(String.join(",", splittedAreaDef), passes); + } + } + logger.info("{}: customArea command needs to have the form customArea:;;;[:x2]", + serialNumber); + } + + return null; + } + + private interface WithDeviceAction { + void run(EcovacsDevice device) throws EcovacsApiException, InterruptedException; + } + + private void doWithDevice(WithDeviceAction action) { + EcovacsDevice device = this.device; + if (device == null) { + return; + } + try { + action.run(device); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (EcovacsApiException e) { + logger.debug("{}: Failed communicating to device, reconnecting", serialNumber, e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + if (e.isAuthFailure) { + EcovacsApiHandler apiHandler = getApiHandler(); + if (apiHandler != null) { + apiHandler.onLoginExpired(); + } + // Drop our device instance to make sure we run a full init cycle, + // including an API re-login, on reconnection + device.disconnect(scheduler); + this.device = null; + } + teardownAndScheduleReconnection(); + } + } + + private @Nullable EcovacsApiHandler getApiHandler() { + final Bridge bridge = getBridge(); + return bridge != null ? (EcovacsApiHandler) bridge.getHandler() : null; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/util/StateOptionEntry.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/util/StateOptionEntry.java new file mode 100644 index 0000000000000..cbb30061c2fdd --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/util/StateOptionEntry.java @@ -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.util; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.ecovacs.internal.api.model.DeviceCapability; + +/** + * A mapping of an binding internal enum value to a user visible (item value) string + * + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public class StateOptionEntry> { + public final T enumValue; + public final String value; + public final Optional capability; + + public StateOptionEntry(T enumValue, String value) { + this(enumValue, value, null); + } + + public StateOptionEntry(T enumValue, String value, @Nullable DeviceCapability capability) { + this.enumValue = enumValue; + this.value = value; + this.capability = Optional.ofNullable(capability); + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/util/StateOptionMapping.java b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/util/StateOptionMapping.java new file mode 100644 index 0000000000000..eeb5d70cc77ff --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/java/org/openhab/binding/ecovacs/internal/util/StateOptionMapping.java @@ -0,0 +1,49 @@ +/** + * 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.util; + +import java.util.HashMap; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Danny Baumann - Initial contribution + */ +@NonNullByDefault +public class StateOptionMapping> extends HashMap> { + private static final long serialVersionUID = -6828690091106259902L; + + public String getMappedValue(T key) { + StateOptionEntry entry = get(key); + if (entry != null) { + return entry.value; + } + throw new IllegalArgumentException("No mapping for key " + key); + } + + public Optional findMappedEnumValue(String value) { + return entrySet().stream().filter(entry -> entry.getValue().value.equals(value)).map(entry -> entry.getKey()) + .findFirst(); + } + + @SafeVarargs + public static > StateOptionMapping of(StateOptionEntry... entries) { + StateOptionMapping map = new StateOptionMapping<>(); + for (StateOptionEntry entry : entries) { + map.put(entry.enumValue, entry); + } + return map; + } +} diff --git a/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..4f3f79b635859 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,11 @@ + + + + binding + Ecovacs Binding + This is the binding for Ecovacs Deebot vacuum cleaners. + cloud + + diff --git a/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/i18n/ecovacs.properties b/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/i18n/ecovacs.properties new file mode 100644 index 0000000000000..4c8227dfd5a53 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/i18n/ecovacs.properties @@ -0,0 +1,179 @@ +# add-on + +addon.ecovacs.name = Ecovacs Binding +addon.ecovacs.description = This is the binding for Ecovacs Deebot vacuum cleaners. + +# thing types + +thing-type.ecovacs.ecovacsapi.label = Ecovacs API Account +thing-type.ecovacs.ecovacsapi.description = The API account +thing-type.ecovacs.vacuum.label = Ecovacs Vacuum Cleaner +thing-type.ecovacs.vacuum.description = Represents an Ecovacs vacuum cleaner + +# thing types config + +thing-type.config.ecovacs.ecovacsapi.continent.label = Continent +thing-type.config.ecovacs.ecovacsapi.continent.description = Continent the account was registered on. Choose the one you are located in, or "World" if none matches. +thing-type.config.ecovacs.ecovacsapi.continent.option.ww = World +thing-type.config.ecovacs.ecovacsapi.continent.option.eu = Europe +thing-type.config.ecovacs.ecovacsapi.continent.option.na = North America +thing-type.config.ecovacs.ecovacsapi.continent.option.as = Asia +thing-type.config.ecovacs.ecovacsapi.email.label = Email +thing-type.config.ecovacs.ecovacsapi.email.description = Email address for logging in to Ecovacs server +thing-type.config.ecovacs.ecovacsapi.password.label = Password +thing-type.config.ecovacs.ecovacsapi.password.description = Password for logging in to Ecovacs server +thing-type.config.ecovacs.vacuum.refresh.label = Refresh Interval +thing-type.config.ecovacs.vacuum.refresh.description = Specifies the refresh interval in minutes. +thing-type.config.ecovacs.vacuum.serialNumber.label = Device Serial Number + +# channel group types + +channel-group-type.ecovacs.actions.label = Actions +channel-group-type.ecovacs.consumables.label = Consumables +channel-group-type.ecovacs.last-clean.label = Last Clean Run +channel-group-type.ecovacs.settings.label = Settings +channel-group-type.ecovacs.status.label = Status +channel-group-type.ecovacs.total-stats.label = Device Lifetime Statistics + +# channel types + +channel-type.ecovacs.auto-empty.label = Auto Empty +channel-type.ecovacs.auto-empty.description = Automatically empty dust bin in station +channel-type.ecovacs.cleaning-passes.label = Cleaning Passes +channel-type.ecovacs.cleaning-passes.description = Number of cleaning passes used by default (if not overridden in command) +channel-type.ecovacs.command.label = Command +channel-type.ecovacs.command.description = Command to execute +channel-type.ecovacs.command.state.option.clean = Automatic cleaning +channel-type.ecovacs.command.state.option.pause = Pause cleaning +channel-type.ecovacs.command.state.option.resume = Resume cleaning +channel-type.ecovacs.command.state.option.stop = Stop +channel-type.ecovacs.command.state.option.charge = Go to charge station +channel-type.ecovacs.continuous-cleaning.label = Continuous Cleaning +channel-type.ecovacs.continuous-cleaning.description = Automatically resume unfinished cleaning after charging +channel-type.ecovacs.current-cleaned-area.label = Current Cleaned Area +channel-type.ecovacs.current-cleaned-area.description = Cleaned area in current clean cycle +channel-type.ecovacs.current-cleaning-mode.label = Current Cleaning Mode +channel-type.ecovacs.current-cleaning-mode.description = Mode used in current clean cycle +channel-type.ecovacs.current-cleaning-spot-definition.label = Current Cleaning Spot +channel-type.ecovacs.current-cleaning-spot-definition.description = Custom or spot area used in current clean cycle +channel-type.ecovacs.current-cleaning-time.label = Current Cleaning Time +channel-type.ecovacs.current-cleaning-time.description = Cleaning time in current clean cycle +channel-type.ecovacs.dust-filter-lifetime.label = Dust Filter Lifetime +channel-type.ecovacs.dust-filter-lifetime.description = Remaining life time of dust bin filter in percent +channel-type.ecovacs.error-code.label = Last Error Code +channel-type.ecovacs.error-code.description = The numerical value (code) of the last encountered error +channel-type.ecovacs.error-description.label = Last Error Description +channel-type.ecovacs.error-description.description = A text describing the last encountered error +channel-type.ecovacs.last-clean-area.label = Last Cleaned Area +channel-type.ecovacs.last-clean-area.description = Cleaned area in last completed cleaning run +channel-type.ecovacs.last-clean-duration.label = Last Cleaning Duration +channel-type.ecovacs.last-clean-duration.description = Duration of last completed cleaning run +channel-type.ecovacs.last-clean-map.label = Last Clean Map +channel-type.ecovacs.last-clean-map.description = Cleaning map for last completed cleaning run +channel-type.ecovacs.last-clean-mode.label = Last Cleaning Mode +channel-type.ecovacs.last-clean-mode.description = Operation mode used in last completed cleaning run +channel-type.ecovacs.last-clean-start.label = Last Cleaning Start +channel-type.ecovacs.last-clean-start.description = Start time of last completed cleaning run +channel-type.ecovacs.main-brush-lifetime.label = Main Brush Lifetime +channel-type.ecovacs.main-brush-lifetime.description = Remaining life time of main brush in percent +channel-type.ecovacs.other-component-lifetime.label = Other Component Lifetime +channel-type.ecovacs.other-component-lifetime.description = Remaining time until device maintenance is required in percent +channel-type.ecovacs.side-brush-lifetime.label = Side Brush Lifetime +channel-type.ecovacs.side-brush-lifetime.description = Remaining life time of side brush in percent +channel-type.ecovacs.state.label = State +channel-type.ecovacs.state.description = Current state +channel-type.ecovacs.state.state.option.cleaning = Cleaning +channel-type.ecovacs.state.state.option.pause = Paused +channel-type.ecovacs.state.state.option.stop = Stopped +channel-type.ecovacs.state.state.option.washing = Washing the cleaning pad +channel-type.ecovacs.state.state.option.drying = Drying the cleaning pad +channel-type.ecovacs.state.state.option.returning = Going to charge station +channel-type.ecovacs.state.state.option.charging = Charging +channel-type.ecovacs.state.state.option.idle = Idle +channel-type.ecovacs.suction-power.label = Cleaning Power Level +channel-type.ecovacs.suction-power.description = Amount of suction power to be used while cleaning +channel-type.ecovacs.suction-power.state.option.silent = Silent +channel-type.ecovacs.suction-power.state.option.normal = Normal +channel-type.ecovacs.suction-power.state.option.high = Maximum +channel-type.ecovacs.suction-power.state.option.higher = Maximum+ +channel-type.ecovacs.total-clean-runs.label = Total Clean Runs +channel-type.ecovacs.total-clean-runs.description = Number of cleaning runs in device life time +channel-type.ecovacs.total-cleaned-area.label = Total Cleaned Area +channel-type.ecovacs.total-cleaned-area.description = Cleaned area in device life time +channel-type.ecovacs.total-cleaning-time.label = Total Cleaning Time +channel-type.ecovacs.total-cleaning-time.description = Cleaning time in device life time +channel-type.ecovacs.true-detect-3d.label = True Detect 3D +channel-type.ecovacs.true-detect-3d.description = Enable the True Detect 3D object recognition technology +channel-type.ecovacs.voice-volume.label = Voice Volume +channel-type.ecovacs.voice-volume.description = Volume level of voice reports +channel-type.ecovacs.water-amount.label = Mopping Water Amount +channel-type.ecovacs.water-amount.state.option.low = Low +channel-type.ecovacs.water-amount.state.option.medium = Medium +channel-type.ecovacs.water-amount.state.option.high = High +channel-type.ecovacs.water-amount.state.option.veryhigh = Very high +channel-type.ecovacs.water-system-present.label = Water System Present +channel-type.ecovacs.water-system-present.description = Water plate with mop attached to device? +channel-type.ecovacs.wifi-rssi.label = Wi-Fi Signal Strength +channel-type.ecovacs.wifi-rssi.description = Received signal strength indicator for Wi-Fi + +# cleaning modes + +ecovacs.cleaning-mode.auto = Automatic +ecovacs.cleaning-mode.edge = Edge cleaning +ecovacs.cleaning-mode.spot = Spot cleaning +ecovacs.cleaning-mode.spotArea = Spot area cleaning +ecovacs.cleaning-mode.customArea = Custom area cleaning +ecovacs.cleaning-mode.singleRoom = Single room cleaning + +# error codes + +ecovacs.vacuum.error-code.0 = No error +ecovacs.vacuum.error-code.3 = Authentication error +ecovacs.vacuum.error-code.7 = Log data was not found +ecovacs.vacuum.error-code.100 = No error +ecovacs.vacuum.error-code.101 = Low battery +ecovacs.vacuum.error-code.102 = Robot is off the floor +ecovacs.vacuum.error-code.103 = Driving wheel malfunction +ecovacs.vacuum.error-code.104 = Excess dust on the anti-drop sensors +ecovacs.vacuum.error-code.105 = Robot is stuck +ecovacs.vacuum.error-code.106 = Side brushes have expired +ecovacs.vacuum.error-code.107 = Dust case filter expired +ecovacs.vacuum.error-code.108 = Side brushes are tangled +ecovacs.vacuum.error-code.109 = Main brush is tangled +ecovacs.vacuum.error-code.110 = Dust bin not installed +ecovacs.vacuum.error-code.111 = Bump sensor stuck +ecovacs.vacuum.error-code.112 = Laser distance sensor malfunction +ecovacs.vacuum.error-code.113 = Main brush has expired +ecovacs.vacuum.error-code.114 = Dust bin full +ecovacs.vacuum.error-code.115 = Battery error +ecovacs.vacuum.error-code.116 = Forward looking error +ecovacs.vacuum.error-code.117 = Gyroscope error +ecovacs.vacuum.error-code.118 = Strainer blocked +ecovacs.vacuum.error-code.119 = Fan error +ecovacs.vacuum.error-code.120 = Water box error +ecovacs.vacuum.error-code.201 = Air filter removed +ecovacs.vacuum.error-code.202 = Ultrasonic component error +ecovacs.vacuum.error-code.203 = Small wheel error +ecovacs.vacuum.error-code.204 = Wheel is blocked +ecovacs.vacuum.error-code.205 = Ion sterilization exhausted +ecovacs.vacuum.error-code.206 = Ion sterilization error +ecovacs.vacuum.error-code.207 = Ion sterilization fault +ecovacs.vacuum.error-code.404 = Recipient unavailable +ecovacs.vacuum.error-code.500 = Request timeout +ecovacs.vacuum.error-code.601 = AIVI side error +ecovacs.vacuum.error-code.602 = AIVI roll error +ecovacs.vacuum.error-code.unknown = Unknown error ({0}) + +# thing status descriptions + +offline.config-error-no-country = A country needs to be set in the openHAB regional settings. +offline.config-error-no-serial = Serial number is missing in the configuration of this device. + +# actions + +playSoundActionLabel = play sound +playSoundActionDesc = Play a sound through the device speaker. +actionInputSoundTypeLabel = Sound Type +actionInputSoundTypeDesc = The type of sound to play. +actionInputSoundIdLabel = Sound ID +actionInputSoundIdDesc = The numeric ID of the sound to play. diff --git a/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/thing/bridge.xml new file mode 100644 index 0000000000000..9c41dba2ea829 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/thing/bridge.xml @@ -0,0 +1,38 @@ + + + + + + + The API account + + + + + email + Email address for logging in to Ecovacs server + true + + + + password + Password for logging in to Ecovacs server + true + + + + Continent the account was registered on. Choose the one you are located in, or "World" if none matches. + ww + + + + + + + + + + diff --git a/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..3bd52094f4598 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,355 @@ + + + + + + + + + + Represents an Ecovacs vacuum cleaner + + + + + + + + + + + + + + + + + true + Specifies the refresh interval in minutes. + Minutes + 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + Current state + + + + + + + + + + + + + + + + String + + Command to execute + + + + + + + + + + + + + String + + Mode used in current clean cycle + + + + + + + + + + + + + + Number:Time + + Cleaning time in current clean cycle + + + + + Number:Area + + Cleaned area in current clean cycle + + + + + String + + Custom or spot area used in current clean cycle + + + + + Number:Time + + Cleaning time in device life time + + + + + Number:Area + + Cleaned area in device life time + + + + + Number + + Number of cleaning runs in device life time + + + + + DateTime + + Start time of last completed cleaning run + + + + + Number:Time + + Duration of last completed cleaning run + + + + + Number:Area + + Cleaned area in last completed cleaning run + + + + + String + + Operation mode used in last completed cleaning run + + + + + + + + + + + + + + Image + + Cleaning map for last completed cleaning run + + + + + Switch + + Automatically empty dust bin in station + + + + Number + + Number of cleaning passes used by default (if not overridden in command) + + + + + Switch + + Automatically resume unfinished cleaning after charging + + + + String + + Amount of suction power to be used while cleaning + + + + + + + + + + + + Switch + + Enable the True Detect 3D object recognition technology + + + + Switch + + Water plate with mop attached to device? + + + + + String + + + + + + + + + + + + + Number:Power + + Received signal strength indicator for Wi-Fi + QualityOfService + + + + + Number:Dimensionless + + Remaining life time of main brush in percent + + + + + Number:Dimensionless + + Remaining life time of side brush in percent + + + + + Number:Dimensionless + + Remaining life time of dust bin filter in percent + + + + + Number:Dimensionless + + Remaining time until device maintenance is required in percent + + + + + Dimmer + + Volume level of voice reports + + + + + Number + + The numerical value (code) of the last encountered error + + + + + String + + A text describing the last encountered error + + + + diff --git a/bundles/org.openhab.binding.ecovacs/src/main/resources/devices/supported_device_list.json b/bundles/org.openhab.binding.ecovacs/src/main/resources/devices/supported_device_list.json new file mode 100644 index 0000000000000..36985c9325559 --- /dev/null +++ b/bundles/org.openhab.binding.ecovacs/src/main/resources/devices/supported_device_list.json @@ -0,0 +1,541 @@ +[ + { + "modelName": "DEEBOT 600 Series", + "deviceClass": "dl8fht", + "protoVersion": "xml", + "usesMqtt": true, + "capabilities": [ + "mopping_system", + "main_brush", + "clean_speed_control" + ] + }, + { + "modelName": "DEEBOT OZMO 601", + "deviceClass": "159", + "deviceClassLink": "dl8fht" + }, + { + "modelName": "DEEBOT 661", + "deviceClass": "16wdph", + "deviceClassLink": "dl8fht" + }, + + { + "modelName": "DEEBOT OZMO 610 Series", + "deviceClass": "130", + "protoVersion": "xml", + "usesMqtt": false, + "capabilities": [ + "mopping_system", + "main_brush", + "single_room_cleaning" + ] + }, + + { + "modelName": "DEEBOT 710", + "deviceClass": "uv242z", + "protoVersion": "xml", + "usesMqtt": true, + "capabilities": [ + "main_brush", + "clean_speed_control" + ] + }, + { + "modelName": "DEEBOT 711", + "deviceClass": "jr3pqa", + "deviceClassLink": "uv242z" + }, + { + "modelName": "DEEBOT 711s", + "deviceClass": "d0cnel", + "deviceClassLink": "uv242z" + }, + { + "modelName": "DEEBOT 715", + "deviceClass": "eyi9jv", + "deviceClassLink": "uv242z" + }, + + { + "modelName": "DEEBOT 900 Series", + "deviceClass": "ls1ok3", + "protoVersion": "xml", + "usesMqtt": true, + "capabilities": [ + "main_brush", + "spot_area_cleaning", + "custom_area_cleaning", + "clean_speed_control", + "mapping" + ] + }, + + { + "modelName": "DEEBOT OZMO 900 Series", + "deviceClass": "y79a7u", + "protoVersion": "xml", + "usesMqtt": true, + "capabilities": [ + "mopping_system", + "main_brush", + "spot_area_cleaning", + "custom_area_cleaning", + "clean_speed_control", + "mapping" + ] + }, + { + "modelName": "DEEBOT OZMO 905", + "deviceClass": "2pv572", + "deviceClassLink": "y79a7u" + }, + + { + "modelName": "DEEBOT OZMO/PRO 930 Series", + "deviceClass": "115", + "protoVersion": "xml", + "usesMqtt": false, + "capabilities": [ + "mopping_system", + "main_brush", + "spot_area_cleaning", + "custom_area_cleaning", + "mapping" + ] + }, + + { + "modelName": "DEEBOT Slim2 Series", + "deviceClass": "123", + "protoVersion": "xml", + "usesMqtt": false, + "capabilities": [ + ] + }, + + { + "modelName": "DEEBOT OZMO Slim10 Series", + "deviceClass": "02uwxm", + "protoVersion": "xml", + "usesMqtt": true, + "capabilities": [ + "mopping_system", + "clean_speed_control" + ] + }, + + { + "modelName": "DEEBOT OZMO 950 Series", + "deviceClass": "yna5xi", + "protoVersion": "json", + "usesMqtt": true, + "capabilities": [ + "mopping_system", + "main_brush", + "spot_area_cleaning", + "custom_area_cleaning", + "clean_speed_control", + "voice_reporting", + "read_network_info", + "mapping" + ] + }, + { + "modelName": "DEEBOT OZMO 920", + "deviceClass": "vi829v", + "deviceClassLink": "yna5xi" + }, + { + "modelName": "DEEBOT OZMO T5", + "deviceClass": "9rft3c", + "deviceClassLink": "yna5xi" + }, + + { + "modelName": "DEEBOT N8", + "deviceClass": "n6cwdb", + "protoVersion": "json_v2", + "usesMqtt": true, + "capabilities": [ + "mopping_system", + "main_brush", + "spot_area_cleaning", + "custom_area_cleaning", + "clean_speed_control", + "voice_reporting", + "read_network_info", + "unit_care_lifespan", + "mapping" + ] + }, + { + "modelName": "DEEBOT N3 MAX", + "deviceClass": "jffnlf", + "deviceClassLink": "n6cwdb" + }, + { + "modelName": "DEEBOT N7", + "deviceClass": "r5zxjr", + "deviceClassLink": "n6cwdb" + }, + { + "modelName": "DEEBOT N8", + "deviceClass": "r5y7re", + "deviceClassLink": "n6cwdb" + }, + { + "modelName": "DEEBOT N8", + "deviceClass": "ty84oi", + "deviceClassLink": "n6cwdb" + }, + { + "modelName": "DEEBOT N8", + "deviceClass": "36xnxf", + "deviceClassLink": "n6cwdb" + }, + { + "modelName": "DEEBOT N8 Neo", + "deviceClass": "z0gd1j", + "deviceClassLink": "n6cwdb" + }, + + { + "modelName": "DEEBOT N8+", + "deviceClass": "b2jqs4", + "protoVersion": "json_v2", + "usesMqtt": true, + "capabilities": [ + "mopping_system", + "main_brush", + "spot_area_cleaning", + "custom_area_cleaning", + "clean_speed_control", + "voice_reporting", + "read_network_info", + "unit_care_lifespan", + "auto_empty_station", + "mapping" + ] + }, + { + "modelName": "DEEBOT N8+", + "deviceClass": "7bryc5", + "deviceClassLink": "b2jqs4" + }, + + { + "modelName": "DEEBOT OZMO T8", + "deviceClass": "h18jkh", + "protoVersion": "json_v2", + "usesMqtt": true, + "capabilities": [ + "mopping_system", + "main_brush", + "spot_area_cleaning", + "custom_area_cleaning", + "clean_speed_control", + "voice_reporting", + "read_network_info", + "unit_care_lifespan", + "true_detect_3d", + "mapping" + ] + }, + { + "modelName": "DEEBOT OZMO T8", + "deviceClass": "b742vd", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT OZMO T8 PURE", + "deviceClass": "0bdtzz", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT OZMO T8 AIVI", + "deviceClass": "x5d34r", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT T8", + "deviceClass": "wgxm70", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT T8 AIVI", + "deviceClass": "bs40nz", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT T8 AIVI", + "deviceClass": "5089oy", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT T8 MAX", + "deviceClass": "a1nNMoAGAsH", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT T8 POWER", + "deviceClass": "no61kx", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT T9", + "deviceClass": "ucn2xe", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT T9", + "deviceClass": "ipohi5", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT N8 PRO", + "deviceClass": "snxbvc", + "deviceClassLink": "h18jkh" + }, + { + "modelName": "DEEBOT N8 PRO", + "deviceClass": "yu362x", + "deviceClassLink": "h18jkh" + }, + + { + "modelName": "DEEBOT OZMO T8+", + "deviceClass": "fqxoiu", + "protoVersion": "json_v2", + "usesMqtt": true, + "capabilities": [ + "mopping_system", + "main_brush", + "spot_area_cleaning", + "custom_area_cleaning", + "clean_speed_control", + "voice_reporting", + "read_network_info", + "unit_care_lifespan", + "true_detect_3d", + "mapping", + "auto_empty_station" + ] + }, + { + "modelName": "DEEBOT OZMO T8+", + "deviceClass": "55aiho", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT OZMO T8 AIVI +", + "deviceClass": "tpnwyu", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT OZMO T8 AIVI +", + "deviceClass": "34vhpm", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT OZMO T8 AIVI +", + "deviceClass": "w16crm", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T8 AIVI +", + "deviceClass": "vdehg6", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T9+", + "deviceClass": "lhbd50", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T9+", + "deviceClass": "um2ywg", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T9 AIVI", + "deviceClass": "8kwdb4", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T9 AIVI", + "deviceClass": "659yh8", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T9 AIVI Plus", + "deviceClass": "kw9ayx", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT N8 PRO+", + "deviceClass": "85as7h", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT N8 PRO+", + "deviceClass": "ifbw08", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT N9+", + "deviceClass": "a7lhb1", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT N9+", + "deviceClass": "c2of2s", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT X1", + "deviceClass": "3yqsch", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T10", + "deviceClass": "jtmf04", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T10 PLUS", + "deviceClass": "rss8xk", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T10 PLUS", + "deviceClass": "p95mgv", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T10 TURBO", + "deviceClass": "9s1s80", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT T10 OMNI", + "deviceClass": "lx3j7m", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT X1 OMNI", + "deviceClass": "8bja83", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT X1 OMNI", + "deviceClass": "1b23du", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT X1 OMNI", + "deviceClass": "1vxt52", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT X1 TURBO", + "deviceClass": "2o4lnm", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT X1 PLUS", + "deviceClass": "n4gstt", + "deviceClassLink": "fqxoiu" + }, + { + "modelName": "DEEBOT X1e OMNI", + "deviceClass": "bro5wu", + "deviceClassLink": "fqxoiu" + }, + + { + "modelName": "DEEBOT U2", + "deviceClass": "ipzjy0", + "protoVersion": "json", + "usesMqtt": true, + "capabilities": [ + "mopping_system", + "main_brush", + "clean_speed_control", + "voice_reporting", + "read_network_info" + ] + }, + { + "modelName": "DEEBOT U2", + "deviceClass": "rvo6ev", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2", + "deviceClass": "wlqdkp", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 PRO", + "deviceClass": "nq9yhl", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 PRO", + "deviceClass": "y2qy3m", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 PRO", + "deviceClass": "7j1tu6", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 PRO", + "deviceClass": "ts2ofl", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 PRO", + "deviceClass": "c0lwyn", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 PRO", + "deviceClass": "d4v1pm", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 PRO", + "deviceClass": "u6eqoa", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 PRO", + "deviceClass": "12baap", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 PRO", + "deviceClass": "u4h1uk", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 POWER", + "deviceClass": "1zqysa", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 POWER", + "deviceClass": "chmi0g", + "deviceClassLink": "ipzjy0" + }, + { + "modelName": "DEEBOT U2 SE", + "deviceClass": "zjna8m", + "deviceClassLink": "ipzjy0" + } +] diff --git a/bundles/pom.xml b/bundles/pom.xml index ec4e0c3193e2d..07c78bca987f4 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -116,6 +116,7 @@ org.openhab.binding.echonetlite org.openhab.binding.ecobee org.openhab.binding.ecotouch + org.openhab.binding.ecovacs org.openhab.binding.ecowatt org.openhab.binding.ekey org.openhab.binding.electroluxair