diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 35085730c71..e4b65e93f82 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1681,6 +1681,11 @@ org.openhab.binding.verisure ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.vesync + ${project.version} + org.openhab.addons.bundles org.openhab.binding.vigicrues diff --git a/bundles/org.openhab.binding.vesync/NOTICE b/bundles/org.openhab.binding.vesync/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/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.vesync/README.md b/bundles/org.openhab.binding.vesync/README.md new file mode 100644 index 00000000000..c8b0742d5bb --- /dev/null +++ b/bundles/org.openhab.binding.vesync/README.md @@ -0,0 +1,303 @@ +# VeSync Binding + +Its current support is for the Air Purifiers & Humidifer's branded as Levoit which utilise the VeSync app based on the V2 protocol. + +### Verified Models + +Air Filtering models supported are Core300S, Core400S. +Air Humidifier models supported are Dual 200S, Classic 300S, 600S. + +### Awaiting User Verification Models + +Air Filtering models supported are Core200S and Core600S. +Air Humidifier Classic 200S (Same as 300S without the nightlight from initial checks) + +## Supported Things + +This binding supports the follow thing types: + +| Thing | Thing Type | Thing Type UID | Discovery | Description | +|----------------|------------|----------------|-----------|----------------------------------------------------------------------| +| Bridge | Bridge | bridge | Manual | A single connection to the VeSync API | +| Air Purifier | Thing | airPurifier | Automatic | A Air Purifier supporting V2 e.g. Core200S/Core300S or Core400S unit | +| Air Humidifier | Thing | airHumidifier | Automatic | A Air Humidifier supporting V2 e.g. Classic300S or 600s | + + + +This binding was developed from the great work in the listed projects. + +The only Air Filter unit it has been tested against is the Core400S unit, **I'm looking for others to confirm** my queries regarding **the Core200S and Core300S** units. +The ***Classic 300S Humidifier*** has been tested, and ***600S with current warm mode restrictions***. + +## Discovery + +Once the bridge is configured auto discovery will discover supported devices from the VeSync API. + +## Thing Configuration + +### Bridge configuration parameters + +| Name | Type | Description | Recommended Values | +|----------------------------------|--------|-----------------------------------------------------------|--------------------| +| username | String | The username as used in the VeSync mobile application | | +| password | String | The password as used in the VeSync mobile application | | +| airPurifierPollInterval | Number | The poll interval (seconds) for air filters / humidifiers | 60 | +| backgroundDeviceDiscovery | Switch | Should the system scan periodically for new devices | ON | +| refreshBackgroundDeviceDiscovery | Number | Frequency (seconds) of scans for new new devices | 120 | + +* Note Air PM Levels don't usually change quickly - 60s seems reasonable if openHAB is controlling it and your don't want near instant feedback of physical interactions with the devices. + +### AirPurifier configuration parameters + +It is recommended to use the device name, for locating devices. For this to work all the devices should have a unique +name in the VeSync mobile application. + +The mac address from the VeSync mobile application may not align to the one the API +uses, therefore it's best left not configured or taken from auto-discovered information. + +Device's will be found communicated with via the MAC Id first and if unsuccessful then by the deviceName. + +| Name | Type | Description | +|------------------------|-------------------------|---------------------------------------------------------------------| +| deviceName | String | The name given to the device under Settings -> Device Name | +| macId | String | The mac for the device under Settings -> Device Info -> MAC Address | + + +## Channels + +Channel names in **bold** are read/write, everything else is read-only + +### AirPurifier Thing + +| Channel | Type | Description | Model's Supported | Controllable Values | +|----------------------|----------------------|------------------------------------------------------------|-------------------|-----------------------| +| **enabled** | Switch | Whether the hardware device is enabled (Switched on) | 600S, 400S, 300S | [ON, OFF] | +| **childLock** | Switch | Whether the child lock (display lock is enabled) | 600S, 400S, 300S | [ON, OFF] | +| **display** | Switch | Whether the display is enabled (display is shown) | 600S, 400S, 300S | [ON, OFF] | +| **fanMode** | String | The operation mode of the fan | 600S, 400S | [auto, manual, sleep] | +| **fanMode** | String | The operation mode of the fan | 200S, 300S, | [manual, sleep] | +| **manualFanSpeed** | Number:Dimensionless | The speed of the fan when in manual mode | 600S, 400S | [1...4] | +| **manualFanSpeed** | Number:Dimensionless | The speed of the fan when in manual mode | 300S | [1...3] | +| **nightLightMode** | String | The night lights mode | 200S, 300S | [on, dim, off] | +| filterLifePercentage | Number:Dimensionless | The remaining filter life as a percentage | 600S, 400S, 300S | | +| airQuality | Number:Dimensionless | The air quality as represented by the Core200S / Core300S | 600S, 400S, 300S | | +| airQualityPM25 | Number:Density | The air quality as represented by the Core400S | 600S, 400S, 300S | | +| errorCode | Number:Dimensionless | The error code reported by the device | 600S, 400S, 300S | | +| timerExpiry | DateTime | The expected expiry time of the current timer | 600S, 400S | | +| schedulesCount | Number:Dimensionless | The number schedules configured | 600S, 400S | | +| configDisplayForever | Switch | Config: Whether the display will disable when not active | 600S, 400S, 300S | | +| configAutoMode | String | Config: The mode of operation when auto is active | 600S, 400S, 300S | | +| configAutoRoomSize | Number:Dimensionless | Config: The room size set when auto utilises the room size | 600S, 400S, 300S | | + + +### AirHumidifier Thing + +| Channel | Type | Description | Model's Supported | Controllable Values | +|----------------------------|----------------------|---------------------------------------------------------------|----------------------------|---------------------| +| **enabled** | Switch | Whether the hardware device is enabled (Switched on) | 200S, Dual200S, 300S, 600S | [ON, OFF] | +| **display** | Switch | Whether the display is enabled (display is shown) | 200S, Dual200S, 300S, 600S | [ON, OFF] | +| waterLacking | Switch | Indicator whether the unit is lacking water | 200S, Dual200S, 300S, 600S | | +| humidityHigh | Switch | Indicator for high humidity | 200S, Dual200S, 300S, 600S | | +| waterTankLifted | Switch | Indicator for whether the water tank is removed | 200S, Dual200S, 300S, 600S | | +| **stopAtHumiditySetpoint** | Switch | Whether the unit is set to stop when the set point is reached | 200S, Dual200S, 300S, 600S | [ON, OFF] | +| humidity | Number:Dimensionless | Indicator for the currently measured humidity % level | 200S, Dual200S, 300S, 600S | | +| **mistLevel** | Number:Dimensionless | The current mist level set | 300S | [1...2] | +| **mistLevel** | Number:Dimensionless | The current mist level set | 200S, Dual200S, 600S | [1...3] | +| **humidifierMode** | String | The current mode of operation | 200S, Dual200S, 300S, 600S | [auto, sleep] | +| **nightLightMode** | String | The night light mode | 200S, Dual200S, 300S | [on, dim, off] | +| **humiditySetpoint** | Number:Dimensionless | Humidity % set point to reach | 200S, Dual200S, 300S, 600S | [30...80] | +| warmEnabled | Switch | Indicator for warm mist mode | 600S | | + + +## Full Example + +### Configuration (*.things) + +#### Air Purifiers Core 200S/300S/400S Models & Air Humidifier Classic300S/600S Models + +``` +Bridge vesync:bridge:vesyncServers [username="", password="", airPurifierPollInterval=60] { + airPurifier loungeAirFilter [deviceName=""] + airPurifier bedroomAirFilter [deviceName=""] + airHumidifier loungeHumidifier [deviceName=""] +} +``` + +### Configuration (*.items) + +#### Air Purifier Core 400S / 600S Model + +``` +Switch LoungeAPPower "Lounge Air Purifier Power" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:enabled" } +Switch LoungeAPDisplay "Lounge Air Purifier Display" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:display" } +Switch LoungeAPControlsLock "Lounge Air Purifier Controls Locked" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:childLock" } +Number:Dimensionless LoungeAPFilterRemainingUse "Lounge Air Purifier Filter Remaining [%.0f %unit%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:filterLifePercentage" } +String LoungeAPMode "Lounge Air Purifier Mode [%s]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:fanMode" } +Number:Dimensionless LoungeAPManualFanSpeed "Lounge Air Purifier Manual Fan Speed" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:manualFanSpeed" } +Number:Density LoungeAPAirQuality "Lounge Air Purifier Air Quality [%.0f% %unit%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:airQualityPM25" } +Number LoungeAPErrorCode "Lounge Air Purifier Error Code" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:errorCode" } +String LoungeAPAutoMode "Lounge Air Purifier Auto Mode" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:configAutoMode" } +Number LoungeAPAutoRoomSize "Lounge Air Purifier Auto Room Size [%.0f% sqft]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:configAutoRoomSize" } +Number:Time LoungeAPTimerLeft "Lounge Air Purifier Timer Left [%1$Tp]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:timerRemain" } +DateTime LoungeAPTimerExpiry "Lounge Air Purifier Timer Expiry [%1$tA %1$tI:%1$tM %1$Tp]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:timerExpiry" } +Number LoungeAPSchedulesCount "Lounge Air Purifier Schedules Count" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:schedulesCount" } +``` + +#### Air Purifier Core 200S/300S Model + +``` +Switch LoungeAPPower "Lounge Air Purifier Power" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:enabled" } +Switch LoungeAPDisplay "Lounge Air Purifier Display" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:display" } +String LoungeAPNightLightMode "Lounge Air Purifier Night Light Mode" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:nightLightMode" } +Switch LoungeAPControlsLock "Lounge Air Purifier Controls Locked" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:childLock" } +Number:Dimensionless LoungeAPFilterRemainingUse "Lounge Air Purifier Filter Remaining [%.0f %unit%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:filterLifePercentage" } +String LoungeAPMode "Lounge Air Purifier Mode [%s]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:fanMode" } +Number:Dimensionless LoungeAPManualFanSpeed "Lounge Air Purifier Manual Fan Speed" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:manualFanSpeed" } +Number:Density LoungeAPAirQuality "Lounge Air Purifier Air Quality [%.0f%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:airQuality" } +Number LoungeAPErrorCode "Lounge Air Purifier Error Code" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:errorCode" } +String LoungeAPAutoMode "Lounge Air Purifier Auto Mode" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:configAutoMode" } +Number LoungeAPAutoRoomSize "Lounge Air Purifier Auto Room Size [%.0f% sqft]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:configAutoRoomSize" } +Number:Time LoungeAPTimerLeft "Lounge Air Purifier Timer Left [%1$Tp]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:timerRemain" } +DateTime LoungeAPTimerExpiry "Lounge Air Purifier Timer Expiry [%1$tA %1$tI:%1$tM %1$Tp]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:timerExpiry" } +Number LoungeAPSchedulesCount "Lounge Air Purifier Schedules Count" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:schedulesCount" } +``` + +#### Air Humidifier Classic 200S / Dual 200S Model + +``` +Switch LoungeAHPower "Lounge Air Humidifier Power" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:enabled" } +Switch LoungeAHDisplay "Lounge Air Humidifier Display" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:display" } +String LoungeAHMode "Lounge Air Humidifier Mode" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidifierMode" } +Switch LoungeAHWaterLacking "Lounge Air Humidifier Water Lacking" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterLacking" } +Switch LoungeAHHighHumidity "Lounge Air Humidifier High Humidity" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidityHigh" } +Switch LoungeAHWaterTankRemoved "Lounge Air Humidifier Water Tank Removed" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterTankLifted" } +Number:Dimensionless LoungeAHHumidity "Lounge Air Humidifier Measured Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidity" } +Switch LoungeAHTargetStop "Lounge Air Humidifier Stop at target" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:stopAtTargetLevel" } +Number:Dimensionless LoungeAHTarget "Lounge Air Humidifier Target Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humiditySetpoint" } +Number:Dimensionless LoungeAHMistLevel "Lounge Air Humidifier Mist Level" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:mistLevel" } +``` + +#### Air Humidifier Classic 300S Model + +``` +Switch LoungeAHPower "Lounge Air Humidifier Power" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:enabled" } +Switch LoungeAHDisplay "Lounge Air Humidifier Display" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:display" } +String LoungeAHNightLightMode "Lounge Air Humidifier Night Light Mode" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:nightLightMode } +String LoungeAHMode "Lounge Air Humidifier Mode" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidifierMode" } +Switch LoungeAHWaterLacking "Lounge Air Humidifier Water Lacking" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterLacking" } +Switch LoungeAHHighHumidity "Lounge Air Humidifier High Humidity" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidityHigh" } +Switch LoungeAHWaterTankRemoved "Lounge Air Humidifier Water Tank Removed" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterTankLifted" } +Number:Dimensionless LoungeAHHumidity "Lounge Air Humidifier Measured Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidity" } +Switch LoungeAHTargetStop "Lounge Air Humidifier Stop at target" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:stopAtTargetLevel" } +Number:Dimensionless LoungeAHTarget "Lounge Air Humidifier Target Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humiditySetpoint" } +Number:Dimensionless LoungeAHMistLevel "Lounge Air Humidifier Mist Level" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:mistLevel" } +``` + +#### Air Humidifier 600S Model + +``` +Switch LoungeAHPower "Lounge Air Humidifier Power" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:enabled" } +Switch LoungeAHDisplay "Lounge Air Humidifier Display" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:display" } +String LoungeAHMode "Lounge Air Humidifier Mode" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidifierMode" } +Switch LoungeAHWaterLacking "Lounge Air Humidifier Water Lacking" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterLacking" } +Switch LoungeAHHighHumidity "Lounge Air Humidifier High Humidity" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidityHigh" } +Switch LoungeAHWaterTankRemoved "Lounge Air Humidifier Water Tank Removed" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterTankLifted" } +Number:Dimensionless LoungeAHHumidity "Lounge Air Humidifier Measured Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidity" } +Switch LoungeAHTargetStop "Lounge Air Humidifier Stop at target" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:stopAtTargetLevel" } +Number:Dimensionless LoungeAHTarget "Lounge Air Humidifier Target Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humiditySetpoint" } +Number:Dimensionless LoungeAHMistLevel "Lounge Air Humidifier Mist Level" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:mistLevel" } +``` + +### Configuration (*.sitemap) + +#### Air Purifier Core 400S / 600S Model + +``` +Frame { + Switch item=LoungeAPPower label="Power" + Text item=LoungeAPFilterRemainingUse label="Filter Remaining" + Switch item=LoungeAPDisplay label="Display" + Text item=LoungeAPAirQuality label="Air Quality [%.0f (PM2.5)]" + Switch item=LoungeAPControlsLock label="Controls Locked" + Text item=LoungeAPTimerExpiry label="Timer Shutdown @" icon="clock" + Switch item=LoungeAPMode label="Mode" mappings=[auto="Auto", manual="Manual Fan Control", sleep="Sleeping"] icon="settings" + Text item=LoungeAPErrorCode label="Error Code [%.0f]" + Switch item=LoungeAPManualFanSpeed label="Manual Fan Speed [%.0f]" mappings=[1="1", 2="2", 3="3", 4="4"] icon="settings" +} +``` + +#### Air Purifier Core 200S/300S Model + +``` +Frame { + Switch item=LoungeAPPower label="Power" + Text item=LoungeAPFilterRemainingUse label="Filter Remaining" + Switch item=LoungeAPDisplay label="Display" + Switch item=LoungeAPNightLightMode label="Night Light Mode" mappings=[on="On", dim="Dimmed", off="Off"] icon="settings" + Text item=LoungeAPAirQuality label="Air Quality [%.0f]" + Switch item=LoungeAPControlsLock label="Controls Locked" + Text item=LoungeAPTimerExpiry label="Timer Shutdown @" icon="clock" + Switch item=LoungeAPMode label="Mode" mappings=[manual="Manual Fan Control", sleep="Sleeping"] icon="settings" + Text item=LoungeAPErrorCode label="Error Code [%.0f]" + Switch item=LoungeAPManualFanSpeed label="Manual Fan Speed [%.0f]" mappings=[1="1", 2="2", 3="3"] icon="settings" +} +``` + +#### Air Humidifier Classic 200S / Dual 200S Model + +``` +Frame { + Switch item=LoungeAHPower + Switch item=LoungeAHDisplay + Switch item=LoungeAHMode label="Mode" mappings=[auto="Auto", sleep="Sleeping"] icon="settings" + Text icon="none" item=LoungeAHWaterLacking + Text icon="none" item=LoungeAHHighHumidity + Text icon="none" item=LoungeAHWaterTankRemoved + Text icon="none" item=LoungeAHHumidity + Switch item=LoungeAHTargetStop + Slider item=LoungeAHTarget minValue=30 maxValue=80 + Slider item=LoungeAHMistLevel minValue=1 maxValue=3 +} +``` + +#### Air Humidifier Classic 300S Model + +``` +Frame { + Switch item=LoungeAHPower + Switch item=LoungeAHDisplay + Switch item=LoungeAHNightLightMode label="Night Light Mode" mappings=[on="On", dim="Dimmed", off="Off"] icon="settings" + Switch item=LoungeAHMode label="Mode" mappings=[auto="Auto", sleep="Sleeping"] icon="settings" + Text icon="none" item=LoungeAHWaterLacking + Text icon="none" item=LoungeAHHighHumidity + Text icon="none" item=LoungeAHWaterTankRemoved + Text icon="none" item=LoungeAHHumidity + Switch item=LoungeAHTargetStop + Slider item=LoungeAHTarget minValue=30 maxValue=80 + Slider item=LoungeAHMistLevel minValue=1 maxValue=3 +} +``` + +#### Air Humidifier 600S Model + +``` +Frame { + Switch item=LoungeAHPower + Switch item=LoungeAHDisplay + Switch item=LoungeAHMode label="Mode" mappings=[auto="Auto", sleep="Sleeping"] icon="settings" + Text icon="none" item=LoungeAHWaterLacking + Text icon="none" item=LoungeAHHighHumidity + Text icon="none" item=LoungeAHWaterTankRemoved + Text icon="none" item=LoungeAHHumidity + Switch item=LoungeAHTargetStop + Slider item=LoungeAHTarget minValue=30 maxValue=80 + Slider item=LoungeAHMistLevel minValue=1 maxValue=3 +} +``` + +### Credits + +The binding code is based on a lot of work done by other developers: + +- Contributors of (https://github.com/webdjoe/pyvesync) - Python interface for VeSync +- Rene Scherer, Holger Eisold - (https://www.openhab.org/addons/bindings/surepetcare) Sure Petcare Binding for openHAB as a reference point for the starting blocks of this code diff --git a/bundles/org.openhab.binding.vesync/pom.xml b/bundles/org.openhab.binding.vesync/pom.xml new file mode 100644 index 00000000000..2e3e1b7d89d --- /dev/null +++ b/bundles/org.openhab.binding.vesync/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.3.0-SNAPSHOT + + + org.openhab.binding.vesync + + openHAB Add-ons :: Bundles :: VeSync Binding + + diff --git a/bundles/org.openhab.binding.vesync/src/main/feature/feature.xml b/bundles/org.openhab.binding.vesync/src/main/feature/feature.xml new file mode 100644 index 00000000000..3d64763fb49 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.vesync/${project.version} + + diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncBridgeConfiguration.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncBridgeConfiguration.java new file mode 100644 index 00000000000..4cd3edbdbc0 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncBridgeConfiguration.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link VeSyncBridgeConfiguration} class contains fields mapping the configuration parameters for the bridge. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VeSyncBridgeConfiguration { + + /** + * The clear text password to access the vesync API. + */ + @Nullable + public String password = ""; + + /** + * The email address / username to access the vesync API. + */ + @Nullable + public String username = ""; + + /** + * The polling interval to use for air purifier devices. + */ + @Nullable + public Integer airPurifierPollInterval; +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncConstants.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncConstants.java new file mode 100644 index 00000000000..137bc9a619b --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncConstants.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * The {@link VeSyncConstants} class defines common constants, which are + * used across the whole binding. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VeSyncConstants { + + public static final Gson GSON = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting() + .disableHtmlEscaping().serializeNulls().create(); + + private static final String BINDING_ID = "vesync"; + + public static final long DEFAULT_REFRESH_INTERVAL_DISCOVERED_DEVICES = 3600; + public static final long DEFAULT_POLL_INTERVAL_AIR_FILTERS_DEVICES = 10; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge"); + public static final ThingTypeUID THING_TYPE_AIR_PURIFIER = new ThingTypeUID(BINDING_ID, "airPurifier"); + public static final ThingTypeUID THING_TYPE_AIR_HUMIDIFIER = new ThingTypeUID(BINDING_ID, "airHumidifier"); + + // Thing configuration properties + public static final String DEVICE_MAC_ID = "macAddress"; + + public static final String EMPTY_STRING = ""; + + // Base Device Channel Names + public static final String DEVICE_CHANNEL_ENABLED = "enabled"; + public static final String DEVICE_CHANNEL_DISPLAY_ENABLED = "display"; + public static final String DEVICE_CHANNEL_CHILD_LOCK_ENABLED = "childLock"; + public static final String DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING = "filterLifePercentage"; + public static final String DEVICE_CHANNEL_FAN_MODE_ENABLED = "fanMode"; + public static final String DEVICE_CHANNEL_FAN_SPEED_ENABLED = "manualFanSpeed"; + public static final String DEVICE_CHANNEL_ERROR_CODE = "errorCode"; + public static final String DEVICE_CHANNEL_AIRQUALITY_BASIC = "airQuality"; + public static final String DEVICE_CHANNEL_AIRQUALITY_PM25 = "airQualityPM25"; + + public static final String DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER = "configDisplayForever"; + public static final String DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF = "configAutoMode"; + + public static final String DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME = "timerExpiry"; + public static final String DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE = "configAutoRoomSize"; + public static final String DEVICE_CHANNEL_AF_SCHEDULES_COUNT = "schedulesCount"; + public static final String DEVICE_CHANNEL_AF_NIGHT_LIGHT = "nightLightMode"; + + // Humidity related channels + public static final String DEVICE_CHANNEL_WATER_LACKS = "waterLacking"; + public static final String DEVICE_CHANNEL_HUMIDITY_HIGH = "humidityHigh"; + public static final String DEVICE_CHANNEL_WATER_TANK_LIFTED = "waterTankLifted"; + public static final String DEVICE_CHANNEL_STOP_AT_TARGET = "stopAtHumiditySetpoint"; + public static final String DEVICE_CHANNEL_HUMIDITY = "humidity"; + public static final String DEVICE_CHANNEL_MIST_LEVEL = "mistLevel"; + public static final String DEVICE_CHANNEL_HUMIDIFIER_MODE = "humidifierMode"; + public static final String DEVICE_CHANNEL_WARM_ENABLED = "warmEnabled"; + public static final String DEVICE_CHANNEL_WARM_LEVEL = "warmLevel"; + + public static final String DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY = "humiditySetpoint"; + + // Property name constants + public static final String DEVICE_PROP_DEVICE_NAME = "Device Name"; + public static final String DEVICE_PROP_DEVICE_TYPE = "Device Type"; + public static final String DEVICE_PROP_DEVICE_MAC_ID = "MAC Id"; + public static final String DEVICE_PROP_DEVICE_UUID = "UUID"; + + // Property name for config constants + public static final String DEVICE_PROP_CONFIG_DEVICE_NAME = "deviceName"; + public static final String DEVICE_PROP_CONFIG_DEVICE_MAC = "macId"; + + // Bridge name constants + public static final String DEVICE_PROP_BRIDGE_REG_TS = "Registration Time"; + public static final String DEVICE_PROP_BRIDGE_COUNTRY_CODE = "Country Code"; + public static final String DEVICE_PROP_BRIDGE_ACCEPT_LANG = "Accept Language"; +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncDeviceConfiguration.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncDeviceConfiguration.java new file mode 100644 index 00000000000..be355f4927b --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncDeviceConfiguration.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link VeSyncDeviceConfiguration} class contains fields mapping the configuration parameters for a VeSync + * device's configuration. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VeSyncDeviceConfiguration { + + /** + * The clear text device name as reported by the API. + */ + @Nullable + public String deviceName; + + /** + * The mac address of the device as reported by the API. + */ + @Nullable + public String macId; +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncHandlerFactory.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncHandlerFactory.java new file mode 100644 index 00000000000..fa660963b10 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncHandlerFactory.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal; + +import static org.openhab.binding.vesync.internal.VeSyncConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.vesync.internal.api.IHttpClientProvider; +import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler; +import org.openhab.binding.vesync.internal.handlers.VeSyncDeviceAirHumidifierHandler; +import org.openhab.binding.vesync.internal.handlers.VeSyncDeviceAirPurifierHandler; +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.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link org.openhab.binding.vesync.internal.VeSyncHandlerFactory} is responsible for creating + * things and thing handlers. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.vesync", service = ThingHandlerFactory.class) +public class VeSyncHandlerFactory extends BaseThingHandlerFactory implements IHttpClientProvider { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, + THING_TYPE_AIR_PURIFIER, THING_TYPE_AIR_HUMIDIFIER); + + private @Nullable HttpClient httpClientRef = null; + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (VeSyncDeviceAirPurifierHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { + return new VeSyncDeviceAirPurifierHandler(thing); + } else if (VeSyncDeviceAirHumidifierHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { + return new VeSyncDeviceAirHumidifierHandler(thing); + } else if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { + return new VeSyncBridgeHandler((Bridge) thing, this); + } + + return null; + } + + @Reference + protected void setHttpClientFactory(HttpClientFactory httpClientFactory) { + httpClientRef = httpClientFactory.getCommonHttpClient(); + } + + @Override + public @Nullable HttpClient getHttpClient() { + return httpClientRef; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/IHttpClientProvider.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/IHttpClientProvider.java new file mode 100644 index 00000000000..c216004509b --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/IHttpClientProvider.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; + +/** + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public interface IHttpClientProvider { + @Nullable + HttpClient getHttpClient(); +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/VeSyncV2ApiHelper.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/VeSyncV2ApiHelper.java new file mode 100644 index 00000000000..1782a630a2f --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/VeSyncV2ApiHelper.java @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.api; + +import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*; + +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.validation.constraints.NotNull; + +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.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncLoginCredentials; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDevicesPage; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncLoginResponse; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDevicesPage; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncResponse; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession; +import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; +import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException; +import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VeSyncV2ApiHelper { + + private final Logger logger = LoggerFactory.getLogger(VeSyncV2ApiHelper.class); + + private @NonNullByDefault({}) HttpClient httpClient; + + private volatile @Nullable VeSyncUserSession loggedInSession; + + private Map macLookup; + + public VeSyncV2ApiHelper() { + macLookup = new HashMap<>(); + } + + public Map getMacLookupMap() { + return macLookup; + } + + /** + * Sets the httpClient object to be used for API calls to Vesync. + * + * @param httpClient the client to be used. + */ + public void setHttpClient(@Nullable HttpClient httpClient) { + this.httpClient = httpClient; + } + + public static @NotNull String calculateMd5(final @Nullable String password) { + if (password == null) { + return ""; + } + MessageDigest md5; + StringBuilder md5Result = new StringBuilder(); + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + return ""; + } + byte[] handshakeHash = md5.digest(password.getBytes(StandardCharsets.UTF_8)); + for (byte handshakeByte : handshakeHash) { + md5Result.append(String.format("%02x", handshakeByte)); + } + return md5Result.toString(); + } + + public void discoverDevices() throws AuthenticationException { + try { + VeSyncRequestManagedDevicesPage reqDevPage = new VeSyncRequestManagedDevicesPage(loggedInSession); + boolean finished = false; + int pageNo = 1; + HashMap generatedMacLookup = new HashMap<>(); + while (!finished) { + reqDevPage.pageNo = String.valueOf(pageNo); + reqDevPage.pageSize = String.valueOf(100); + final String result = reqV1Authorized(V1_MANAGED_DEVICES_ENDPOINT, reqDevPage); + + VeSyncManagedDevicesPage resultsPage = VeSyncConstants.GSON.fromJson(result, + VeSyncManagedDevicesPage.class); + if (resultsPage == null || !resultsPage.outcome.getTotal().equals(resultsPage.outcome.getPageSize())) { + finished = true; + } else { + ++pageNo; + } + + if (resultsPage != null) { + for (VeSyncManagedDeviceBase device : resultsPage.outcome.list) { + logger.debug( + "Found device : {}, type: {}, deviceType: {}, connectionState: {}, deviceStatus: {}, deviceRegion: {}, cid: {}, configModule: {}, macID: {}, uuid: {}", + device.getDeviceName(), device.getType(), device.getDeviceType(), + device.getConnectionStatus(), device.getDeviceStatus(), device.getDeviceRegion(), + device.getCid(), device.getConfigModule(), device.getMacId(), device.getUuid()); + + // Update the mac address -> device table + generatedMacLookup.put(device.getMacId(), device); + } + } + } + macLookup = Collections.unmodifiableMap(generatedMacLookup); + } catch (final AuthenticationException ae) { + logger.warn("Failed background device scan : {}", ae.getMessage()); + throw ae; + } + } + + public String reqV2Authorized(final String url, final String macId, final VeSyncAuthenticatedRequest requestData) + throws AuthenticationException, DeviceUnknownException { + if (loggedInSession == null) { + throw new AuthenticationException("User is not logged in"); + } + // Apply current session authentication data + requestData.applyAuthentication(loggedInSession); + + // Apply specific addressing parameters + if (requestData instanceof VeSyncRequestManagedDeviceBypassV2) { + final VeSyncManagedDeviceBase deviceData = macLookup.get(macId); + if (deviceData == null) { + throw new DeviceUnknownException(String.format("Device not discovered with mac id: %s", macId)); + } + ((VeSyncRequestManagedDeviceBypassV2) requestData).cid = deviceData.cid; + ((VeSyncRequestManagedDeviceBypassV2) requestData).configModule = deviceData.configModule; + ((VeSyncRequestManagedDeviceBypassV2) requestData).deviceRegion = deviceData.deviceRegion; + } + return reqV1Authorized(url, requestData); + } + + public String reqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData) + throws AuthenticationException { + try { + return directReqV1Authorized(url, requestData); + } catch (final AuthenticationException ae) { + throw ae; + } + } + + private String directReqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData) + throws AuthenticationException { + try { + Request request = httpClient.POST(url); + + // No headers for login + request.content(new StringContentProvider(VeSyncConstants.GSON.toJson(requestData))); + + logger.debug("POST @ {} with content\r\n{}", url, VeSyncConstants.GSON.toJson(requestData)); + + request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8"); + + ContentResponse response = request.timeout(5, TimeUnit.SECONDS).send(); + if (response.getStatus() == HttpURLConnection.HTTP_OK) { + VeSyncResponse commResponse = VeSyncConstants.GSON.fromJson(response.getContentAsString(), + VeSyncResponse.class); + + if (commResponse != null && (commResponse.isMsgSuccess() || commResponse.isMsgDeviceOffline())) { + logger.debug("Got OK response {}", response.getContentAsString()); + return response.getContentAsString(); + } else { + logger.debug("Got FAILED response {}", response.getContentAsString()); + throw new AuthenticationException("Invalid JSON response from login"); + } + } else { + logger.debug("HTTP Response Code: {}", response.getStatus()); + logger.debug("HTTP Response Msg: {}", response.getReason()); + throw new AuthenticationException( + "HTTP response " + response.getStatus() + " - " + response.getReason()); + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + throw new AuthenticationException(e); + } + } + + public synchronized void login(final @Nullable String username, final @Nullable String password, + final @Nullable String timezone) throws AuthenticationException { + if (username == null || password == null || timezone == null) { + loggedInSession = null; + return; + } + try { + loggedInSession = processLogin(username, password, timezone).getUserSession(); + } catch (final AuthenticationException ae) { + loggedInSession = null; + throw ae; + } + } + + public void updateBridgeData(final VeSyncBridgeHandler bridge) { + bridge.handleNewUserSession(loggedInSession); + } + + private VeSyncLoginResponse processLogin(String username, String password, String timezone) + throws AuthenticationException { + try { + Request request = httpClient.POST(V1_LOGIN_ENDPOINT); + + // No headers for login + request.content(new StringContentProvider( + VeSyncConstants.GSON.toJson(new VeSyncLoginCredentials(username, password)))); + + request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8"); + + ContentResponse response = request.timeout(5, TimeUnit.SECONDS).send(); + if (response.getStatus() == HttpURLConnection.HTTP_OK) { + VeSyncLoginResponse loginResponse = VeSyncConstants.GSON.fromJson(response.getContentAsString(), + VeSyncLoginResponse.class); + if (loginResponse != null && loginResponse.isMsgSuccess()) { + logger.debug("Login successful"); + return loginResponse; + } else { + throw new AuthenticationException("Invalid / unexpected JSON response from login"); + } + } else { + logger.warn("Login Failed - HTTP Response Code: {} - {}", response.getStatus(), response.getReason()); + throw new AuthenticationException( + "HTTP response " + response.getStatus() + " - " + response.getReason()); + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + throw new AuthenticationException(e); + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/discovery/DeviceMetaDataUpdatedHandler.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/discovery/DeviceMetaDataUpdatedHandler.java new file mode 100644 index 00000000000..977ca3c46b6 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/discovery/DeviceMetaDataUpdatedHandler.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.discovery; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler; + +/** + * The {@link DeviceMetaDataUpdatedHandler} enables call-backs for when the device meta-data is updated from a bridge. + * (VeSync Server Account). + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public interface DeviceMetaDataUpdatedHandler { + void handleMetadataRetrieved(VeSyncBridgeHandler handler); +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/discovery/VeSyncDiscoveryService.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/discovery/VeSyncDiscoveryService.java new file mode 100644 index 00000000000..a601a14d397 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/discovery/VeSyncDiscoveryService.java @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.discovery; + +import static org.openhab.binding.vesync.internal.VeSyncConstants.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingTypeUID; +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; + +/** + * The {@link VeSyncDiscoveryService} is an implementation of a discovery service for VeSync devices. The meta-data is + * read by the bridge, and the discovery data updated via a callback implemented by the DeviceMetaDataUpdatedHandler. + * + * @author David Godyear - Initial contribution + */ +@NonNullByDefault +@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.vesync") +public class VeSyncDiscoveryService extends AbstractDiscoveryService + implements DiscoveryService, ThingHandlerService, DeviceMetaDataUpdatedHandler { + + private static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE); + + private static final int DISCOVER_TIMEOUT_SECONDS = 5; + + private @NonNullByDefault({}) VeSyncBridgeHandler bridgeHandler; + private @NonNullByDefault({}) ThingUID bridgeUID; + + /** + * Creates a VeSyncDiscoveryService with enabled autostart. + */ + public VeSyncDiscoveryService() { + super(SUPPORTED_THING_TYPES, DISCOVER_TIMEOUT_SECONDS); + } + + @Override + public Set getSupportedThingTypes() { + return SUPPORTED_THING_TYPES; + } + + @Override + public void activate() { + final Map properties = new HashMap<>(); + properties.put(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, Boolean.TRUE); + super.activate(properties); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof VeSyncBridgeHandler) { + bridgeHandler = (VeSyncBridgeHandler) handler; + bridgeUID = bridgeHandler.getUID(); + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } + + @Override + protected void startBackgroundDiscovery() { + if (bridgeHandler != null) { + bridgeHandler.registerMetaDataUpdatedHandler(this); + } + } + + @Override + protected void stopBackgroundDiscovery() { + if (bridgeHandler != null) { + bridgeHandler.unregisterMetaDataUpdatedHandler(this); + } + } + + @Override + protected void startScan() { + // If the bridge is not online no other thing devices can be found, so no reason to scan at this moment. + removeOlderResults(getTimestampOfLastScan()); + if (ThingStatus.ONLINE.equals(bridgeHandler.getThing().getStatus())) { + bridgeHandler.runDeviceScanSequenceNoAuthErrors(); + } + } + + @Override + public void handleMetadataRetrieved(VeSyncBridgeHandler handler) { + bridgeHandler.getAirPurifiersMetadata().map(apMeta -> { + final Map properties = new HashMap<>(6); + final String deviceUUID = apMeta.getUuid(); + properties.put(DEVICE_PROP_DEVICE_NAME, apMeta.getDeviceName()); + properties.put(DEVICE_PROP_DEVICE_TYPE, apMeta.getDeviceType()); + properties.put(DEVICE_PROP_DEVICE_MAC_ID, apMeta.getMacId()); + properties.put(DEVICE_PROP_DEVICE_UUID, deviceUUID); + properties.put(DEVICE_PROP_CONFIG_DEVICE_MAC, apMeta.getMacId()); + properties.put(DEVICE_PROP_CONFIG_DEVICE_NAME, apMeta.getDeviceName()); + return DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_AIR_PURIFIER, bridgeUID, deviceUUID)) + .withLabel(apMeta.getDeviceName()).withBridge(bridgeUID).withProperties(properties) + .withRepresentationProperty(DEVICE_PROP_DEVICE_MAC_ID).build(); + }).forEach(this::thingDiscovered); + + bridgeHandler.getAirHumidifiersMetadata().map(apMeta -> { + final Map properties = new HashMap<>(6); + final String deviceUUID = apMeta.getUuid(); + properties.put(DEVICE_PROP_DEVICE_NAME, apMeta.getDeviceName()); + properties.put(DEVICE_PROP_DEVICE_TYPE, apMeta.getDeviceType()); + properties.put(DEVICE_PROP_DEVICE_MAC_ID, apMeta.getMacId()); + properties.put(DEVICE_PROP_DEVICE_UUID, deviceUUID); + properties.put(DEVICE_PROP_CONFIG_DEVICE_MAC, apMeta.getMacId()); + properties.put(DEVICE_PROP_CONFIG_DEVICE_NAME, apMeta.getDeviceName()); + return DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_AIR_HUMIDIFIER, bridgeUID, deviceUUID)) + .withLabel(apMeta.getDeviceName()).withBridge(bridgeUID).withProperties(properties) + .withRepresentationProperty(DEVICE_PROP_DEVICE_MAC_ID).build(); + }).forEach(this::thingDiscovered); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/VeSyncBridgeConfiguration.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/VeSyncBridgeConfiguration.java new file mode 100644 index 00000000000..9cb73a88858 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/VeSyncBridgeConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto; + +import static org.openhab.binding.vesync.internal.VeSyncConstants.DEFAULT_POLL_INTERVAL_AIR_FILTERS_DEVICES; +import static org.openhab.binding.vesync.internal.VeSyncConstants.DEFAULT_REFRESH_INTERVAL_DISCOVERED_DEVICES; + +/** + * The {@link VeSyncBridgeConfiguration} is a container for all the bridge configuration. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncBridgeConfiguration { + + public String username; + public String password; + public long airPurifierPollInterval = DEFAULT_POLL_INTERVAL_AIR_FILTERS_DEVICES; + public boolean backgroundDeviceDiscovery; + public long refreshBackgroundDeviceDiscovery = DEFAULT_REFRESH_INTERVAL_DISCOVERED_DEVICES; +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncAuthenticatedRequest.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncAuthenticatedRequest.java new file mode 100644 index 00000000000..45f0f3f4862 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncAuthenticatedRequest.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession; +import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncAuthenticatedRequest} is a Java class used as a DTO to hold the Vesync's API's common request data. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncAuthenticatedRequest extends VeSyncRequest { + + @SerializedName("accountID") + public String accountId; + + @SerializedName("token") + public String token; + + public VeSyncAuthenticatedRequest() { + super(); + } + + public VeSyncAuthenticatedRequest(final VeSyncUserSession user) throws AuthenticationException { + super(); + if (user == null) { + throw new AuthenticationException("User is not logged in"); + } + this.token = user.getToken(); + this.accountId = user.getAccountId(); + } + + public void applyAuthentication(final VeSyncUserSession userSession) throws AuthenticationException { + if (userSession == null) { + throw new AuthenticationException("User is not logged in"); + } + this.accountId = userSession.getAccountId(); + this.token = userSession.getToken(); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncLoginCredentials.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncLoginCredentials.java new file mode 100644 index 00000000000..21cb5327197 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncLoginCredentials.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncLoginCredentials} is the Java class as a DTO to hold login credentials for the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncLoginCredentials extends VeSyncRequest { + + @SerializedName("email") + public String email; + @SerializedName("password") + public String passwordMd5; + @SerializedName("userType") + public String userType; + @SerializedName("devToken") + public String devToken = ""; + + public VeSyncLoginCredentials() { + super(); + userType = "1"; + method = "login"; + } + + public VeSyncLoginCredentials(String email, String password) { + this(); + this.email = email; + this.passwordMd5 = password; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncProtocolConstants.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncProtocolConstants.java new file mode 100644 index 00000000000..02078cc940d --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncProtocolConstants.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +/** + * The {@link VeSyncProtocolConstants} contains common Strings used by various elements of the protocol. + * + * @author David Goodyear - Initial contribution + */ +public interface VeSyncProtocolConstants { + + // Common Payloads + String MODE_AUTO = "auto"; + String MODE_MANUAL = "manual"; + String MODE_SLEEP = "sleep"; + + String MODE_ON = "on"; + String MODE_DIM = "dim"; + String MODE_OFF = "off"; + + // Common Commands + String DEVICE_SET_SWITCH = "setSwitch"; + String DEVICE_SET_DISPLAY = "setDisplay"; + String DEVICE_SET_LEVEL = "setLevel"; + + // Humidifier Commands + String DEVICE_SET_AUTOMATIC_STOP = "setAutomaticStop"; + String DEVICE_SET_HUMIDITY_MODE = "setHumidityMode"; + String DEVICE_SET_TARGET_HUMIDITY_MODE = "setTargetHumidity"; + String DEVICE_SET_VIRTUAL_LEVEL = "setVirtualLevel"; + String DEVICE_SET_NIGHT_LIGHT_BRIGHTNESS = "setNightLightBrightness"; + String DEVICE_GET_HUMIDIFIER_STATUS = "getHumidifierStatus"; + + String DEVICE_LEVEL_TYPE_MIST = "mist"; + + // Air Purifier Commands + String DEVICE_SET_PURIFIER_MODE = "setPurifierMode"; + String DEVICE_SET_CHILD_LOCK = "setChildLock"; + String DEVICE_SET_NIGHT_LIGHT = "setNightLight"; + String DEVICE_GET_PURIFIER_STATUS = "getPurifierStatus"; + String DEVICE_LEVEL_TYPE_WIND = "wind"; + + /** + * Base URL for AUTHENTICATION REQUESTS + */ + String PROTOCOL = "https"; + String HOST_ENDPOINT = PROTOCOL + "://smartapi.vesync.com/cloud"; + String V1_LOGIN_ENDPOINT = HOST_ENDPOINT + "/v1/user/login"; + String V1_MANAGED_DEVICES_ENDPOINT = HOST_ENDPOINT + "/v1/deviceManaged/devices"; + String V2_BYPASS_ENDPOINT = HOST_ENDPOINT + "/v2/deviceManaged/bypassV2"; +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequest.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequest.java new file mode 100644 index 00000000000..3b693c807da --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequest.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncRequest} is a Java class used as a DTO to hold the Vesync's API's common request data. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequest { + + @SerializedName("timeZone") + public String timeZone = "America/New_York"; + + @SerializedName("acceptLanguage") + public String acceptLanguage = "en"; + + @SerializedName("appVersion") + public String appVersion = "2.5.1"; + + @SerializedName("phoneBrand") + public String phoneBrand = "SM N9005"; + + @SerializedName("phoneOS") + public String phoneOS = "Android"; + + @SerializedName("traceId") + public String traceId = ""; + + @SerializedName("method") + public String method; + + public VeSyncRequest() { + traceId = String.valueOf(System.currentTimeMillis()); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestManagedDeviceBypassV2.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestManagedDeviceBypassV2.java new file mode 100644 index 00000000000..569203ec8d9 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestManagedDeviceBypassV2.java @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncRequestManagedDeviceBypassV2} is a Java class used as a DTO to hold the Vesync's API's common + * request data for V2 ByPass payloads. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestManagedDeviceBypassV2 extends VeSyncAuthenticatedRequest { + + @SerializedName("deviceRegion") + public String deviceRegion = ""; + + @SerializedName("debugMode") + public boolean debugMode = false; + + @SerializedName("cid") + public String cid = ""; + + @SerializedName("configModule") + public String configModule = ""; + + @SerializedName("payload") + public VesyncManagedDeviceBase payload = new VesyncManagedDeviceBase(); + + /** + * Contains basic information about the device. + */ + public class VesyncManagedDeviceBase { + + @SerializedName("method") + public String method; + + @SerializedName("source") + public String source = "APP"; + + @SerializedName("data") + public EmptyPayload data = new EmptyPayload(); + } + + public static class EmptyPayload { + } + + public static class SetSwitchPayload extends EmptyPayload { + + public SetSwitchPayload(final boolean enabled, final int id) { + this.enabled = enabled; + this.id = id; + } + + @SerializedName("enabled") + public boolean enabled = true; + + @SerializedName("id") + public int id = -1; + } + + public static class EnabledPayload extends EmptyPayload { + + public EnabledPayload(final boolean enabled) { + this.enabled = enabled; + } + + @SerializedName("enabled") + public boolean enabled = true; + } + + public static class SetLevelPayload extends EmptyPayload { + + public SetLevelPayload(final int id, final String type, final int level) { + this.id = id; + this.type = type; + this.level = level; + } + + @SerializedName("id") + public int id = -1; + + @SerializedName("level") + public int level = -1; + + @SerializedName("type") + public String type = ""; + } + + public static class SetState extends EmptyPayload { + + public SetState(final boolean state) { + this.state = state; + } + + @SerializedName("state") + public boolean state = false; + } + + public static class SetNightLight extends EmptyPayload { + + public SetNightLight(final String state) { + this.nightLight = state; + } + + @SerializedName("night_light") + public String nightLight = ""; + } + + public static class SetNightLightBrightness extends EmptyPayload { + + public SetNightLightBrightness(final int state) { + this.nightLightLevel = state; + } + + @SerializedName("night_light_brightness") + public int nightLightLevel = 0; + } + + public static class SetTargetHumidity extends EmptyPayload { + + public SetTargetHumidity(final int state) { + this.targetHumidity = state; + } + + @SerializedName("target_humidity") + public int targetHumidity = 0; + } + + public static class SetChildLock extends EmptyPayload { + + public SetChildLock(final boolean childLock) { + this.childLock = childLock; + } + + @SerializedName("child_lock") + public boolean childLock = false; + } + + public static class SetMode extends EmptyPayload { + + public SetMode(final String mode) { + this.mode = mode; + } + + @SerializedName("mode") + public String mode = ""; + } + + public VeSyncRequestManagedDeviceBypassV2() { + super(); + method = "bypassV2"; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestManagedDevicesPage.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestManagedDevicesPage.java new file mode 100644 index 00000000000..fa92d21f308 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestManagedDevicesPage.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession; +import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncRequestManagedDevicesPage} is the Java class as a DTO to hold login credentials for the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestManagedDevicesPage extends VeSyncAuthenticatedRequest { + + @SerializedName("pageNo") + public String pageNo; + + @SerializedName("pageSize") + public String pageSize; + + public VeSyncRequestManagedDevicesPage(final VeSyncUserSession user) throws AuthenticationException { + super(user); + method = "devices"; + } + + public VeSyncRequestManagedDevicesPage(final VeSyncUserSession user, int pageNo, int pageSize) + throws AuthenticationException { + this(user); + this.pageNo = String.valueOf(pageNo); + this.pageSize = String.valueOf(pageSize); + } + + public String getPageNo() { + return pageNo; + } + + public String getPageSize() { + return pageSize; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1ManagedDeviceDetails.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1ManagedDeviceDetails.java new file mode 100644 index 00000000000..5a2a950cacf --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1ManagedDeviceDetails.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession; +import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncRequestV1ManagedDeviceDetails} is the Java class as a DTO to hold login credentials for the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestV1ManagedDeviceDetails extends VeSyncAuthenticatedRequest { + + @SerializedName("mobileId") + public String mobileId = "1234567890123456"; + + @SerializedName("uuid") + public String uuid = null; + + public VeSyncRequestV1ManagedDeviceDetails(final String deviceUuid) { + uuid = deviceUuid; + method = "deviceDetail"; + } + + public VeSyncRequestV1ManagedDeviceDetails(final VeSyncUserSession user) throws AuthenticationException { + super(user); + method = "deviceDetail"; + } + + public VeSyncRequestV1ManagedDeviceDetails(final VeSyncUserSession user, String deviceUuid) + throws AuthenticationException { + this(user); + uuid = deviceUuid; + } + + public String getUuid() { + return uuid; + } + + public String getMobileId() { + return mobileId; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncLoginResponse.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncLoginResponse.java new file mode 100644 index 00000000000..fef91fd0a69 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncLoginResponse.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +/** + * The {@link VeSyncLoginResponse} is a Java class used as a DTO to hold the Vesync's API's login response. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncLoginResponse extends VeSyncResponse { + + public VeSyncUserSession result; + + public VeSyncUserSession getUserSession() { + return result; + } + + public String getToken() { + return (result == null) ? null : result.token; + } + + public String getAccountId() { + return (result == null) ? null : result.accountId; + } + + @Override + public String toString() { + return "VesyncLoginResponse [msg=" + getMsg() + ", result=" + result + "]"; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncManagedDeviceBase.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncManagedDeviceBase.java new file mode 100644 index 00000000000..7f0823adc2a --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncManagedDeviceBase.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * Contains basic information about a single device, from within a VeSyncManagedDevicesPage. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncManagedDeviceBase { + + @SerializedName("deviceRegion") + public String deviceRegion; + + public String getDeviceRegion() { + return deviceRegion; + } + + @SerializedName("deviceType") + public String deviceType; + + public String getDeviceType() { + return deviceType; + } + + @SerializedName("deviceName") + public String deviceName; + + public String getDeviceName() { + return deviceName; + } + + @SerializedName("deviceImg") + public String deviceImg; + + public String getDeviceImg() { + return deviceImg; + } + + @SerializedName("deviceStatus") + public String deviceStatus; + + public String getDeviceStatus() { + return deviceStatus; + } + + @SerializedName("cid") + public String cid; + + public String getCid() { + return cid; + } + + @SerializedName("connectionStatus") + public String connectionStatus; + + public String getConnectionStatus() { + return connectionStatus; + } + + @SerializedName("connectionType") + public String connectionType; + + public String getConnectionType() { + return connectionType; + } + + @SerializedName("type") + public String type; + + public String getType() { + return type; + } + + @SerializedName("subDeviceNo") + public String subDeviceNo; + + public String getSubDeviceNo() { + return subDeviceNo; + } + + @SerializedName("subDeviceType") + public String subDeviceType; + + public String getSubDeviceType() { + return subDeviceType; + } + + @SerializedName("uuid") + public String uuid; + + public String getUuid() { + return uuid; + } + + @SerializedName("macID") + public String macId; + + public String getMacId() { + return macId; + } + + @SerializedName("currentFirmVersion") + public String currentFirmVersion; + + public String getCurrentFirmVersion() { + return currentFirmVersion; + } + + @SerializedName("configModule") + public String configModule; + + public String getConfigModule() { + return configModule; + } + + @SerializedName("mode") + public String mode; + + public String getMode() { + return mode; + } + + @SerializedName("speed") + public String speed; + + public String getSpeed() { + return speed; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncManagedDevicesPage.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncManagedDevicesPage.java new file mode 100644 index 00000000000..0505715d2a5 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncManagedDevicesPage.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncManagedDevicesPage} is a Java class used as a DTO to hold the Vesync's API's response data to a + * page of data requesting the manages devices. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncManagedDevicesPage extends VeSyncResponse { + + @SerializedName("result") + public Outcome outcome; + + public class Outcome { + @SerializedName("pageNo") + public String pageNo; + + @SerializedName("total") + public String total; + + @SerializedName("pageSize") + public String pageSize; + + @SerializedName("list") + public VeSyncManagedDeviceBase[] list; + + public String getPageNo() { + return pageNo; + } + + public String getPageSize() { + return pageSize; + } + + public String getTotal() { + return total; + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncResponse.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncResponse.java new file mode 100644 index 00000000000..1370a630454 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncResponse.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncResponse} is a Java class used as a DTO to hold the Vesync's API's common response data. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncResponse { + + @SerializedName("traceId") + public String traceId; + + @SerializedName("code") + public String code; + + @SerializedName("msg") + public String msg; + + public String getMsg() { + return msg; + } + + public String getTraceId() { + return traceId; + } + + public String getCode() { + return code; + } + + public boolean isMsgSuccess() { + return (msg != null) ? "request success".equals(msg) : false; + } + + public boolean isMsgDeviceOffline() { + return (msg != null) ? "device offline".equals(msg) : false; + } + + @Override + public String toString() { + return "VesyncResponse [traceId=\"" + traceId + "\", msg=\"" + msg + "\", code=\"" + code + "\"]"; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncResponseManagedDeviceBypassV2.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncResponseManagedDeviceBypassV2.java new file mode 100644 index 00000000000..d540f08ee34 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncResponseManagedDeviceBypassV2.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncResponseManagedDeviceBypassV2} is a Java class used as a DTO to hold the Vesync's API's common + * response data. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncResponseManagedDeviceBypassV2 extends VeSyncResponse { + + @SerializedName("result") + public ManagedDeviceByPassV2Payload result; + + public class ManagedDeviceByPassV2Payload extends VeSyncResponse { + + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncUserSession.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncUserSession.java new file mode 100644 index 00000000000..2b69ba6e103 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncUserSession.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * Contains data about the logged in user - including the accountID and token's used + * for authenticating other payload's. + * + * @see unit test - Result may not be in respone if not authenticated + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncUserSession { + + public String token; + + public String getToken() { + return token; + } + + @SerializedName("registerTime") + public String registerTime; + + @SerializedName("accountID") + public String accountId; + + public String getAccountId() { + return accountId; + } + + @SerializedName("registerAppVersion") + public String registerAppVersion; + + @SerializedName("countryCode") + public String countryCode; + + @SerializedName("acceptLanguage") + public String acceptLanguage; + + @Override + public String toString() { + return "Data [user=AB" + ", token=" + token + "]"; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassHumidifierStatus.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassHumidifierStatus.java new file mode 100644 index 00000000000..6c0a41a0ad3 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassHumidifierStatus.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncV2BypassHumidifierStatus} is a Java class used as a DTO to hold the Vesync's API's common response + * data, in regards to a Air Humidifier device. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncV2BypassHumidifierStatus extends VeSyncResponse { + + @SerializedName("result") + public HumidifierrStatus result; + + public class HumidifierrStatus extends VeSyncResponse { + + @SerializedName("result") + public AirHumidifierStatus result; + + public class AirHumidifierStatus { + @SerializedName("enabled") + public boolean enabled; + + @SerializedName("humidity") + public int humidity; + + @SerializedName("mist_virtual_level") + public int mistVirtualLevel; + + @SerializedName("mist_level") + public int mistLevel; + + @SerializedName("mode") + public String mode; + + @SerializedName("water_lacks") + public boolean waterLacks; + + @SerializedName("humidity_high") + public boolean humidityHigh; + + @SerializedName("water_tank_lifted") + public boolean waterTankLifted; + + @SerializedName("display") + public boolean display; + + @SerializedName("automatic_stop_reach_target") + public boolean automaticStopReachTarget; + + @SerializedName("configuration") + public HumidityPurifierConfig configuration; + + @SerializedName("night_light_brightness") + public int nightLightBrightness; + + @SerializedName("warm_enabled") + public boolean warnEnabled; + + @SerializedName("warm_level") + public int warmLevel; + + public class HumidityPurifierConfig { + @SerializedName("auto_target_humidity") + public int autoTargetHumidity; + + @SerializedName("display") + public boolean display; + + @SerializedName("automatic_stop") + public boolean automaticStop; + } + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatus.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatus.java new file mode 100644 index 00000000000..ce883674d9e --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatus.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncV2BypassPurifierStatus} is a Java class used as a DTO to hold the Vesync's API's common response + * data, + * in regards to a Air Purifier device. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncV2BypassPurifierStatus extends VeSyncResponse { + + @SerializedName("result") + public PurifierStatus result; + + public class PurifierStatus extends VeSyncResponse { + + @SerializedName("result") + public AirPurifierStatus result; + + public class AirPurifierStatus { + @SerializedName("enabled") + public boolean enabled; + + @SerializedName("filter_life") + public int filterLife; + + @SerializedName("mode") + public String mode; + + @SerializedName("level") + public int level; + + @SerializedName("air_quality") + public int airQuality; + + @SerializedName("air_quality_value") + public int airQualityValue; + + @SerializedName("display") + public boolean display; + + @SerializedName("child_lock") + public boolean childLock; + + @SerializedName("night_light") + public String nightLight; + + @SerializedName("configuration") + public AirPurifierConfig configuration; + + public class AirPurifierConfig { + @SerializedName("display") + public boolean display; + + @SerializedName("display_forever") + public boolean displayForever; + + @SerializedName("auto_preference") + public AirPurifierConfigAutoPref autoPreference; + + public class AirPurifierConfigAutoPref { + @SerializedName("type") + public String autoType; + + @SerializedName("room_size") + public int roomSize; + } + } + + @SerializedName("extension") + public AirPurifierExtension extension; + + public class AirPurifierExtension { + @SerializedName("schedule_count") + public int scheduleCount; + + @SerializedName("timer_remain") + public int timerRemain; + } + + @SerializedName("device_error_code") + public int deviceErrorCode; + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/v1/VeSyncV1AirPurifierDeviceDetailsResponse.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/v1/VeSyncV1AirPurifierDeviceDetailsResponse.java new file mode 100644 index 00000000000..b907caa17c2 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/v1/VeSyncV1AirPurifierDeviceDetailsResponse.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses.v1; + +import org.openhab.binding.vesync.internal.dto.responses.VeSyncResponse; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncV1AirPurifierDeviceDetailsResponse} is a Java class used as a DTO to hold the Vesync's V1 API's + * common response + * data, in regards to a Air Purifier device. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncV1AirPurifierDeviceDetailsResponse extends VeSyncResponse { + + @SerializedName("screenStatus") + public String screenStatus; + + public String getScreenStatus() { + return screenStatus; + } + + @SerializedName("airQuality") + public int airQuality; + + public int getAirQuality() { + return airQuality; + } + + @SerializedName("level") + public int level; + + public int getLevel() { + return level; + } + + @SerializedName("mode") + public String mode; + + public String getMode() { + return mode; + } + + @SerializedName("deviceName") + public String deviceName; + + public String getDeviceName() { + return deviceName; + } + + @SerializedName("currentFirmVersion") + public String currentFirmVersion; + + public String getCurrentFirmVersion() { + return currentFirmVersion; + } + + @SerializedName("childLock") + public String childLock; + + public String getChildLock() { + return childLock; + } + + @SerializedName("deviceStatus") + public String deviceStatus; + + public String getDeviceStatus() { + return deviceStatus; + } + + @SerializedName("deviceImg") + public String deviceImgUrl; + + public String getDeviceImgUrl() { + return deviceImgUrl; + } + + @SerializedName("connectionStatus") + public String connectionStatus; + + public String getConnectionStatus() { + return connectionStatus; + } + + public boolean isDeviceOnline() { + return "online".equals(connectionStatus); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/exceptions/AuthenticationException.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/exceptions/AuthenticationException.java new file mode 100644 index 00000000000..5b9d6ddd1fa --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/exceptions/AuthenticationException.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link AuthenticationException} is thrown if the authentication/login process is unsuccessful. + * + * @author David Godyear - Initial contribution + */ +@NonNullByDefault +public class AuthenticationException extends Exception { + + private static final long serialVersionUID = -7786425895604150557L; + + public AuthenticationException() { + super(); + } + + public AuthenticationException(final String message) { + super(message); + } + + public AuthenticationException(final Throwable cause) { + super(cause); + } + + public AuthenticationException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/exceptions/DeviceUnknownException.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/exceptions/DeviceUnknownException.java new file mode 100644 index 00000000000..783ff3f9f33 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/exceptions/DeviceUnknownException.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link DeviceUnknownException} is thrown if the device information could not be located for the address in + * relation + * to the API. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class DeviceUnknownException extends Exception { + + private static final long serialVersionUID = -7786425642285150557L; + + public DeviceUnknownException() { + super(); + } + + public DeviceUnknownException(final String message) { + super(message); + } + + public DeviceUnknownException(final Throwable cause) { + super(cause); + } + + public DeviceUnknownException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBaseDeviceHandler.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBaseDeviceHandler.java new file mode 100644 index 00000000000..681c4fe597e --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBaseDeviceHandler.java @@ -0,0 +1,508 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handlers; + +import static org.openhab.binding.vesync.internal.VeSyncConstants.*; +import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.V2_BYPASS_ENDPOINT; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration; +import org.openhab.binding.vesync.internal.VeSyncDeviceConfiguration; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase; +import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; +import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +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.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link VeSyncBaseDeviceHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(VeSyncBaseDeviceHandler.class); + + private static final String MARKER_INVALID_DEVICE_KEY = "---INVALID---"; + + @NotNull + protected String deviceLookupKey = MARKER_INVALID_DEVICE_KEY; + + private static final int CACHE_TIMEOUT_SECOND = 5; + + private int activePollRate = -2; // -1 is used to deactivate the poll, so default to a different value + + private @Nullable ScheduledFuture backgroundPollingScheduler; + private final Object pollConfigLock = new Object(); + + protected @Nullable VeSyncClient veSyncClient; + + private volatile long latestReadBackMillis = 0; + + @Nullable + ScheduledFuture initialPollingTask = null; + + @Nullable + ScheduledFuture readbackPollTask = null; + + public VeSyncBaseDeviceHandler(Thing thing) { + super(thing); + } + + protected @Nullable Channel findChannelById(final String channelGroupId) { + return getThing().getChannel(channelGroupId); + } + + protected ExpiringCache lastPollResultCache = new ExpiringCache<>(Duration.ofSeconds(CACHE_TIMEOUT_SECOND), + VeSyncBaseDeviceHandler::expireCacheContents); + + private static @Nullable String expireCacheContents() { + return null; + } + + @Override + public void channelLinked(ChannelUID channelUID) { + super.channelLinked(channelUID); + + scheduler.execute(this::pollForUpdate); + } + + protected void setBackgroundPollInterval(final int seconds) { + if (activePollRate == seconds) { + return; + } + logger.debug("Reconfiguring devices background polling to {} seconds", seconds); + + synchronized (pollConfigLock) { + final ScheduledFuture job = backgroundPollingScheduler; + + // Cancel the current scan's and re-schedule as required + if (job != null && !job.isCancelled()) { + job.cancel(true); + backgroundPollingScheduler = null; + } + if (seconds > 0) { + logger.trace("Device data is polling every {} seconds", seconds); + backgroundPollingScheduler = scheduler.scheduleWithFixedDelay(this::pollForUpdate, seconds, seconds, + TimeUnit.SECONDS); + } + activePollRate = seconds; + } + } + + public boolean requiresMetaDataFrequentUpdates() { + return (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)); + } + + private @Nullable BridgeHandler getBridgeHandler() { + Bridge bridgeRef = getBridge(); + if (bridgeRef == null) { + return null; + } else { + return bridgeRef.getHandler(); + } + } + + protected boolean isDeviceOnline() { + BridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) { + VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler; + @Nullable + VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap().get(deviceLookupKey); + + if (metadata == null) { + return false; + } + + return ("online".equals(metadata.connectionStatus)); + } + return false; + } + + public void updateDeviceMetaData() { + Map newProps = null; + + BridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) { + VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler; + @Nullable + VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap().get(deviceLookupKey); + + if (metadata == null) { + return; + } + + newProps = getMetadataProperities(metadata); + + // Refresh the device -> protocol mapping + deviceLookupKey = getValidatedIdString(); + + if ("online".equals(metadata.connectionStatus)) { + updateStatus(ThingStatus.ONLINE); + } else if ("offline".equals(metadata.connectionStatus)) { + updateStatus(ThingStatus.OFFLINE); + } + } + + if (newProps != null && !newProps.isEmpty()) { + this.updateProperties(newProps); + removeChannels(); + if (!isDeviceSupported()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Device Model or Type not supported by this thing"); + } + } + } + + /** + * Override this in classes that extend this, to + */ + protected void customiseChannels() { + } + + protected String[] getChannelsToRemove() { + return new String[] {}; + } + + private void removeChannels() { + final String[] channelsToRemove = getChannelsToRemove(); + final List channelsToBeRemoved = new ArrayList<>(); + for (String name : channelsToRemove) { + Channel ch = getThing().getChannel(name); + if (ch != null) { + channelsToBeRemoved.add(ch); + } + } + + final ThingBuilder builder = editThing().withoutChannels(channelsToBeRemoved); + updateThing(builder.build()); + } + + /** + * Extract the common properties for all devices, from the given meta-data of a device. + * + * @param metadata - the meta-data of a device + * @return - Map of common props + */ + public Map getMetadataProperities(final @Nullable VeSyncManagedDeviceBase metadata) { + if (metadata == null) { + return Map.of(); + } + final Map newProps = new HashMap<>(4); + newProps.put(DEVICE_PROP_DEVICE_MAC_ID, metadata.getMacId()); + newProps.put(DEVICE_PROP_DEVICE_NAME, metadata.getDeviceName()); + newProps.put(DEVICE_PROP_DEVICE_TYPE, metadata.getDeviceType()); + newProps.put(DEVICE_PROP_DEVICE_UUID, metadata.getUuid()); + return newProps; + } + + protected synchronized @Nullable VeSyncClient getVeSyncClient() { + if (veSyncClient == null) { + Bridge bridge = getBridge(); + if (bridge == null) { + return null; + } + ThingHandler handler = bridge.getHandler(); + if (handler instanceof VeSyncClient) { + veSyncClient = (VeSyncClient) handler; + } else { + return null; + } + } + return veSyncClient; + } + + protected void requestBridgeFreqScanMetadataIfReq() { + if (requiresMetaDataFrequentUpdates()) { + BridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) { + VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler; + vesyncBridgeHandler.checkIfIncreaseScanRateRequired(); + } + } + } + + @NotNull + public String getValidatedIdString() { + final VeSyncDeviceConfiguration config = getConfigAs(VeSyncDeviceConfiguration.class); + + BridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) { + VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler; + + final String configMac = config.macId; + + // Try to use the mac directly + if (configMac != null) { + logger.debug("Searching for device mac id : {}", configMac); + @Nullable + VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap() + .get(configMac.toLowerCase()); + + if (metadata != null && metadata.macId != null) { + return metadata.macId; + } + } + + final String deviceName = config.deviceName; + + // Check if the device name can be matched to a single device + if (deviceName != null) { + final String[] matchedMacIds = vesyncBridgeHandler.api.getMacLookupMap().values().stream() + .filter(x -> deviceName.equals(x.deviceName)).map(x -> x.macId).toArray(String[]::new); + + for (String val : matchedMacIds) { + logger.debug("Found MAC match on name with : {}", val); + } + + if (matchedMacIds.length != 1) { + return MARKER_INVALID_DEVICE_KEY; + } + + if (vesyncBridgeHandler.api.getMacLookupMap().get(matchedMacIds[0]) != null) { + return matchedMacIds[0]; + } + } + } + + return MARKER_INVALID_DEVICE_KEY; + } + + @Override + public void initialize() { + intializeDeviceForUse(); + } + + private void intializeDeviceForUse() { + // Sanity check basic setup + final VeSyncBridgeHandler bridge = (VeSyncBridgeHandler) getBridgeHandler(); + if (bridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Missing bridge for API link"); + return; + } else { + updateStatus(ThingStatus.UNKNOWN); + } + + deviceLookupKey = getValidatedIdString(); + + // Populate device props - this is required for polling, to cross-check the device model. + updateDeviceMetaData(); + + // If the base device class marks it as offline there is an issue that will prevent normal operation + if (getThing().getStatus().equals(ThingStatus.OFFLINE)) { + return; + } + // This will force the bridge to push the configuration parameters for polling to the handler + bridge.updateThing(this); + + // Give the bridge time to build the datamaps of the devices + scheduleInitialPoll(); + } + + private void scheduleInitialPoll() { + cancelInitialPoll(false); + initialPollingTask = scheduler.schedule(this::pollForUpdate, 10, TimeUnit.SECONDS); + } + + private void cancelInitialPoll(final boolean interruptAllowed) { + final ScheduledFuture pollJob = initialPollingTask; + if (pollJob != null && !pollJob.isCancelled()) { + pollJob.cancel(interruptAllowed); + initialPollingTask = null; + } + } + + private void cancelReadbackPoll(final boolean interruptAllowed) { + final ScheduledFuture pollJob = readbackPollTask; + if (pollJob != null && !pollJob.isCancelled()) { + pollJob.cancel(interruptAllowed); + readbackPollTask = null; + } + } + + @Override + public void dispose() { + cancelReadbackPoll(true); + cancelInitialPoll(true); + } + + public void pollForUpdate() { + pollForDeviceData(lastPollResultCache); + } + + /** + * This should be implemented by subclasses to provide the implementation for polling the specific + * data for the type the class is responsible for. (Excluding meta data). + * + * @param cachedResponse - An Expiring cache that can be utilised to store the responses, to prevent poll bursts by + * coalescing the requests. + */ + protected abstract void pollForDeviceData(final ExpiringCache cachedResponse); + + /** + * Send a BypassV2 command to the device. The body of the response is returned, a poll is done if the request + * should have been dispatched. + * + * @param method - the V2 bypass method + * @param payload - The payload to send in within the V2 bypass command + * @return - The body of the response, or EMPTY_STRING if the command could not be issued. + */ + protected final String sendV2BypassControlCommand(final String method, + final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload) { + return sendV2BypassControlCommand(method, payload, true); + } + + /** + * Send a BypassV2 command to the device. The body of the response is returned. + * + * @param method - the V2 bypass method + * @param payload - The payload to send in within the V2 bypass command + * @param readbackDevice - if set to true after the command has been issued, whether a poll of the devices data + * should be run. + * @return - The body of the response, or EMPTY_STRING if the command could not be issued. + */ + protected final String sendV2BypassControlCommand(final String method, + final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload, final boolean readbackDevice) { + final String result = sendV2BypassCommand(method, payload); + if (!result.equals(EMPTY_STRING) && readbackDevice) { + performReadbackPoll(); + } + return result; + } + + public final String sendV1Command(final String method, final String url, final VeSyncAuthenticatedRequest request) { + if (ThingStatus.OFFLINE.equals(this.thing.getStatus())) { + logger.debug("Command blocked as device is offline"); + return EMPTY_STRING; + } + + try { + if (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)) { + deviceLookupKey = getValidatedIdString(); + } + VeSyncClient client = getVeSyncClient(); + if (client != null) { + return client.reqV2Authorized(url, deviceLookupKey, request); + } else { + throw new DeviceUnknownException("Missing client"); + } + } catch (AuthenticationException e) { + logger.debug("Auth exception {}", e.getMessage()); + return EMPTY_STRING; + } catch (final DeviceUnknownException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Check configuration details - " + e.getMessage()); + // In case the name is updated server side - request the scan rate is increased + requestBridgeFreqScanMetadataIfReq(); + return EMPTY_STRING; + } + } + + /** + * Send a BypassV2 command to the device. The body of the response is returned. + * + * @param method - the V2 bypass method + * @param payload - The payload to send in within the V2 bypass command + * @return - The body of the response, or EMPTY_STRING if the command could not be issued. + */ + protected final String sendV2BypassCommand(final String method, + final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload) { + if (ThingStatus.OFFLINE.equals(this.thing.getStatus())) { + logger.debug("Command blocked as device is offline"); + return EMPTY_STRING; + } + + VeSyncRequestManagedDeviceBypassV2 readReq = new VeSyncRequestManagedDeviceBypassV2(); + readReq.payload.method = method; + readReq.payload.data = payload; + + try { + if (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)) { + deviceLookupKey = getValidatedIdString(); + } + VeSyncClient client = getVeSyncClient(); + if (client != null) { + return client.reqV2Authorized(V2_BYPASS_ENDPOINT, deviceLookupKey, readReq); + } else { + throw new DeviceUnknownException("Missing client"); + } + } catch (AuthenticationException e) { + logger.debug("Auth exception {}", e.getMessage()); + return EMPTY_STRING; + } catch (final DeviceUnknownException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Check configuration details - " + e.getMessage()); + // In case the name is updated server side - request the scan rate is increased + requestBridgeFreqScanMetadataIfReq(); + return EMPTY_STRING; + } + } + + // Given several changes may be done at the same time, or in close proximity, delay the read-back to catch + // multiple read-back's, so a single update can handle them. + public void performReadbackPoll() { + final long requestSystemMillis = System.currentTimeMillis(); + latestReadBackMillis = requestSystemMillis; + cancelReadbackPoll(false); + readbackPollTask = scheduler.schedule(() -> { + // This is a historical poll, ignore it + if (requestSystemMillis != latestReadBackMillis) { + logger.trace("Poll read-back cancelled, another later one is scheduled to happen"); + return; + } + logger.trace("Read-back poll executing"); + // Read-backs should never use the cached data - but may provide it for poll's that coincide with + // the caches alive duration. + lastPollResultCache.invalidateValue(); + pollForUpdate(); + }, 1L, TimeUnit.SECONDS); + } + + public void updateBridgeBasedPolls(VeSyncBridgeConfiguration config) { + } + + /** + * Subclasses should implement this method, and return true if the device is a model it can support + * interoperability with. If it cannot be determind to be a mode + * + * @return - true if the device is supported, false if the device isn't. E.g. Unknown model id in meta-data would + * return false. + */ + protected abstract boolean isDeviceSupported(); +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBridgeHandler.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBridgeHandler.java new file mode 100644 index 00000000000..6294dccae1d --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBridgeHandler.java @@ -0,0 +1,241 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handlers; + +import static org.openhab.binding.vesync.internal.VeSyncConstants.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration; +import org.openhab.binding.vesync.internal.api.IHttpClientProvider; +import org.openhab.binding.vesync.internal.api.VeSyncV2ApiHelper; +import org.openhab.binding.vesync.internal.discovery.DeviceMetaDataUpdatedHandler; +import org.openhab.binding.vesync.internal.discovery.VeSyncDiscoveryService; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession; +import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; +import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException; +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.ThingUID; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link VeSyncBridgeHandler} is responsible for handling the bridge things created to use the VeSync + * API. This way, the user credentials may be entered only once. + * + * @author David Goodyear - Initial Contribution + */ +@NonNullByDefault +public class VeSyncBridgeHandler extends BaseBridgeHandler implements VeSyncClient { + + private static final int DEFAULT_DEVICE_SCAN_INTERVAL = 600; + private static final int DEFAULT_DEVICE_SCAN_RECOVERY_INTERVAL = 60; + private static final int DEFAULT_DEVICE_SCAN_DISABLED = -1; + + private final Logger logger = LoggerFactory.getLogger(VeSyncBridgeHandler.class); + + private @Nullable ScheduledFuture backgroundDiscoveryPollingJob; + + protected final VeSyncV2ApiHelper api = new VeSyncV2ApiHelper(); + private IHttpClientProvider httpClientProvider; + + private volatile int backgroundScanTime = -1; + private final Object scanConfigLock = new Object(); + + public VeSyncBridgeHandler(Bridge bridge, @NotNull IHttpClientProvider httpClientProvider) { + super(bridge); + this.httpClientProvider = httpClientProvider; + } + + public ThingUID getUID() { + return thing.getUID(); + } + + protected void checkIfIncreaseScanRateRequired() { + logger.trace("Checking if increased background scanning for new devices / base information is required"); + boolean frequentScanReq = false; + for (Thing th : getThing().getThings()) { + ThingHandler handler = th.getHandler(); + if (handler instanceof VeSyncBaseDeviceHandler) { + if (((VeSyncBaseDeviceHandler) handler).requiresMetaDataFrequentUpdates()) { + frequentScanReq = true; + break; + } + } + } + + if (!frequentScanReq + && api.getMacLookupMap().values().stream().anyMatch(x -> "offline".equals(x.connectionStatus))) { + frequentScanReq = true; + } + + if (frequentScanReq) { + setBackgroundScanInterval(DEFAULT_DEVICE_SCAN_RECOVERY_INTERVAL); + } else { + setBackgroundScanInterval(DEFAULT_DEVICE_SCAN_INTERVAL); + } + } + + protected void setBackgroundScanInterval(final int seconds) { + synchronized (scanConfigLock) { + ScheduledFuture job = backgroundDiscoveryPollingJob; + if (backgroundScanTime != seconds) { + if (seconds > 0) { + logger.trace("Scheduling background scanning for new devices / base information every {} seconds", + seconds); + } else { + logger.trace("Disabling background scanning for new devices / base information"); + } + // Cancel the current scan's and re-schedule as required + if (job != null && !job.isCancelled()) { + job.cancel(true); + backgroundDiscoveryPollingJob = null; + } + if (seconds > 0) { + backgroundDiscoveryPollingJob = scheduler.scheduleWithFixedDelay( + this::runDeviceScanSequenceNoAuthErrors, seconds, seconds, TimeUnit.SECONDS); + } + backgroundScanTime = seconds; + } + } + } + + public void registerMetaDataUpdatedHandler(DeviceMetaDataUpdatedHandler dmduh) { + handlers.add(dmduh); + } + + public void unregisterMetaDataUpdatedHandler(DeviceMetaDataUpdatedHandler dmduh) { + handlers.remove(dmduh); + } + + private final CopyOnWriteArrayList handlers = new CopyOnWriteArrayList<>(); + + public void runDeviceScanSequenceNoAuthErrors() { + try { + runDeviceScanSequence(); + updateStatus(ThingStatus.ONLINE); + } catch (AuthenticationException ae) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check login credentials"); + } + } + + public void runDeviceScanSequence() throws AuthenticationException { + logger.trace("Scanning for new devices / base information now"); + api.discoverDevices(); + handlers.forEach(x -> x.handleMetadataRetrieved(this)); + checkIfIncreaseScanRateRequired(); + + this.updateThings(); + } + + public java.util.stream.Stream<@NotNull VeSyncManagedDeviceBase> getAirPurifiersMetadata() { + return api.getMacLookupMap().values().stream() + .filter(x -> VeSyncDeviceAirPurifierHandler.SUPPORTED_DEVICE_TYPES.contains(x.deviceType)); + } + + public java.util.stream.Stream<@NotNull VeSyncManagedDeviceBase> getAirHumidifiersMetadata() { + return api.getMacLookupMap().values().stream() + .filter(x -> VeSyncDeviceAirHumidifierHandler.SUPPORTED_DEVICE_TYPES.contains(x.deviceType)); + } + + protected void updateThings() { + final VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class); + getThing().getThings().forEach((th) -> updateThing(config, th.getHandler())); + } + + public void updateThing(ThingHandler handler) { + final VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class); + updateThing(config, handler); + } + + private void updateThing(VeSyncBridgeConfiguration config, @Nullable ThingHandler handler) { + if (handler instanceof VeSyncBaseDeviceHandler) { + ((VeSyncBaseDeviceHandler) handler).updateDeviceMetaData(); + ((VeSyncBaseDeviceHandler) handler).updateBridgeBasedPolls(config); + } + } + + @Override + public Collection> getServices() { + return Collections.singleton(VeSyncDiscoveryService.class); + } + + @Override + public void initialize() { + api.setHttpClient(httpClientProvider.getHttpClient()); + + VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class); + + scheduler.submit(() -> { + final String passwordMd5 = VeSyncV2ApiHelper.calculateMd5(config.password); + + try { + api.login(config.username, passwordMd5, "Europe/London"); + api.updateBridgeData(this); + runDeviceScanSequence(); + updateStatus(ThingStatus.ONLINE); + } catch (final AuthenticationException ae) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check login credentials"); + // The background scan will keep trying to authenticate in case the users credentials are updated on the + // veSync servers, + // to match the binding's configuration. + } + }); + } + + @Override + public void dispose() { + setBackgroundScanInterval(DEFAULT_DEVICE_SCAN_DISABLED); + api.setHttpClient(null); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.warn("Handling command for VeSync bridge handler."); + } + + public void handleNewUserSession(final @Nullable VeSyncUserSession userSessionData) { + final Map newProps = new HashMap<>(); + if (userSessionData != null) { + newProps.put(DEVICE_PROP_BRIDGE_REG_TS, userSessionData.registerTime); + newProps.put(DEVICE_PROP_BRIDGE_COUNTRY_CODE, userSessionData.countryCode); + newProps.put(DEVICE_PROP_BRIDGE_ACCEPT_LANG, userSessionData.acceptLanguage); + } + this.updateProperties(newProps); + } + + public String reqV2Authorized(final String url, final String macId, final VeSyncAuthenticatedRequest requestData) + throws AuthenticationException, DeviceUnknownException { + return api.reqV2Authorized(url, macId, requestData); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncClient.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncClient.java new file mode 100644 index 00000000000..2eb1c5ab52d --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncClient.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handlers; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest; +import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; +import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException; + +/** + * The {@link VeSyncClient} is TBC. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public interface VeSyncClient { + String reqV2Authorized(final String url, final String macId, final VeSyncAuthenticatedRequest requestData) + throws AuthenticationException, DeviceUnknownException; +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceAirHumidifierHandler.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceAirHumidifierHandler.java new file mode 100644 index 00000000000..30ca49fb0f4 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceAirHumidifierHandler.java @@ -0,0 +1,349 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handlers; + +import static org.openhab.binding.vesync.internal.VeSyncConstants.*; +import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassHumidifierStatus; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link VeSyncDeviceAirHumidifierHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler { + + public static final int DEFAULT_AIR_PURIFIER_POLL_RATE = 120; + // "Device Type" values + public static final String DEV_TYPE_DUAL_200S = "Dual200S"; + public static final String DEV_TYPE_CLASSIC_200S = "Classic200S"; + public static final String DEV_TYPE_CORE_301S = "LUH-D301S-WEU"; + public static final String DEV_TYPE_CLASSIC_300S = "Classic300S"; + public static final String DEV_TYPE_600S = "LUH-A602S-WUS"; + public static final String DEV_TYPE_600S_EU = "LUH-A602S-WEU"; + + private static final List CLASSIC_300S_600S_MODES = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP); + private static final List CLASSIC_300S_NIGHT_LIGHT_MODES = Arrays.asList(MODE_ON, MODE_DIM, MODE_OFF); + + public static final List SUPPORTED_DEVICE_TYPES = List.of(DEV_TYPE_DUAL_200S, DEV_TYPE_CLASSIC_200S, + DEV_TYPE_CLASSIC_300S, DEV_TYPE_CORE_301S, DEV_TYPE_600S, DEV_TYPE_600S_EU); + + private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirHumidifierHandler.class); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIR_HUMIDIFIER); + + private final Object pollLock = new Object(); + + public VeSyncDeviceAirHumidifierHandler(Thing thing) { + super(thing); + } + + @Override + protected String[] getChannelsToRemove() { + String[] toRemove = new String[] {}; + final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE); + if (deviceType != null) { + switch (deviceType) { + case DEV_TYPE_CLASSIC_300S: + toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL }; + break; + case DEV_TYPE_DUAL_200S: + case DEV_TYPE_CLASSIC_200S: + case DEV_TYPE_CORE_301S: + toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL, + DEVICE_CHANNEL_AF_NIGHT_LIGHT }; + break; + case DEV_TYPE_600S: + case DEV_TYPE_600S_EU: + toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT }; + break; + } + } + return toRemove; + } + + @Override + public void initialize() { + super.initialize(); + customiseChannels(); + } + + @Override + public void updateBridgeBasedPolls(final VeSyncBridgeConfiguration config) { + Integer pollRate = config.airPurifierPollInterval; + if (pollRate == null) { + pollRate = DEFAULT_AIR_PURIFIER_POLL_RATE; + } + if (ThingStatus.OFFLINE.equals(getThing().getStatus())) { + setBackgroundPollInterval(-1); + } else { + setBackgroundPollInterval(pollRate); + } + } + + @Override + public void dispose() { + this.setBackgroundPollInterval(-1); + } + + @Override + protected boolean isDeviceSupported() { + final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE); + if (deviceType == null) { + return false; + } + return SUPPORTED_DEVICE_TYPES.contains(deviceType); + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE); + if (deviceType == null) { + return; + } + + scheduler.submit(() -> { + + if (command instanceof OnOffType) { + switch (channelUID.getId()) { + case DEVICE_CHANNEL_ENABLED: + sendV2BypassControlCommand(DEVICE_SET_SWITCH, + new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload(command.equals(OnOffType.ON), + 0)); + break; + case DEVICE_CHANNEL_DISPLAY_ENABLED: + sendV2BypassControlCommand(DEVICE_SET_DISPLAY, + new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON))); + break; + case DEVICE_CHANNEL_STOP_AT_TARGET: + sendV2BypassControlCommand(DEVICE_SET_AUTOMATIC_STOP, + new VeSyncRequestManagedDeviceBypassV2.EnabledPayload(command.equals(OnOffType.ON))); + break; + case DEVICE_CHANNEL_WARM_ENABLED: + logger.warn("Warm mode API is unknown in order to send the command"); + break; + } + } else if (command instanceof QuantityType) { + switch (channelUID.getId()) { + case DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY: + int targetHumidity = ((QuantityType) command).intValue(); + if (targetHumidity < 30) { + logger.warn("Target Humidity less than 30 - adjusting to 30 as the valid API value"); + targetHumidity = 30; + } else if (targetHumidity > 80) { + logger.warn("Target Humidity greater than 80 - adjusting to 80 as the valid API value"); + targetHumidity = 80; + } + + sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_AUTO), false); + + sendV2BypassControlCommand(DEVICE_SET_TARGET_HUMIDITY_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetTargetHumidity(targetHumidity)); + break; + case DEVICE_CHANNEL_MIST_LEVEL: + int targetMistLevel = ((QuantityType) command).intValue(); + // If more devices have this the hope is it's those with the prefix LUH so the check can + // be simplified, originally devices mapped 1/5/9 to 1/2/3. + if (DEV_TYPE_CORE_301S.equals(deviceType)) { + if (targetMistLevel < 1) { + logger.warn("Target Mist Level less than 1 - adjusting to 1 as the valid API value"); + targetMistLevel = 1; + } else if (targetMistLevel > 2) { + logger.warn("Target Mist Level greater than 2 - adjusting to 2 as the valid API value"); + targetMistLevel = 2; + } + } else { + if (targetMistLevel < 1) { + logger.warn("Target Mist Level less than 1 - adjusting to 1 as the valid API value"); + targetMistLevel = 1; + } else if (targetMistLevel > 3) { + logger.warn("Target Mist Level greater than 3 - adjusting to 3 as the valid API value"); + targetMistLevel = 3; + } + // Re-map to what appears to be bitwise encoding of the states + switch (targetMistLevel) { + case 1: + targetMistLevel = 1; + break; + case 2: + targetMistLevel = 5; + break; + case 3: + targetMistLevel = 9; + break; + } + } + + sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false); + + sendV2BypassControlCommand(DEVICE_SET_VIRTUAL_LEVEL, + new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_MIST, + targetMistLevel)); + break; + case DEVICE_CHANNEL_WARM_LEVEL: + logger.warn("Warm level API is unknown in order to send the command"); + break; + } + } else if (command instanceof StringType) { + final String targetMode = command.toString().toLowerCase(); + switch (channelUID.getId()) { + case DEVICE_CHANNEL_HUMIDIFIER_MODE: + if (!CLASSIC_300S_600S_MODES.contains(targetMode)) { + logger.warn( + "Humidifier mode command for \"{}\" is not valid in the (Classic300S/600S) API possible options {}", + command, String.join(",", CLASSIC_300S_NIGHT_LIGHT_MODES)); + return; + } + sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetMode(targetMode)); + break; + case DEVICE_CHANNEL_AF_NIGHT_LIGHT: + if (!DEV_TYPE_CLASSIC_300S.equals(deviceType) && !DEV_TYPE_CORE_301S.equals(deviceType)) { + logger.warn("Humidifier night light is not valid for your device ({}})", deviceType); + return; + } + if (!CLASSIC_300S_NIGHT_LIGHT_MODES.contains(targetMode)) { + logger.warn( + "Humidifier night light mode command for \"{}\" is not valid in the (Classic300S) API possible options {}", + command, String.join(",", CLASSIC_300S_NIGHT_LIGHT_MODES)); + return; + } + int targetValue; + switch (targetMode) { + case MODE_OFF: + targetValue = 0; + break; + case MODE_DIM: + targetValue = 50; + break; + case MODE_ON: + targetValue = 100; + break; + default: + return; // should never hit + } + sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT_BRIGHTNESS, + new VeSyncRequestManagedDeviceBypassV2.SetNightLightBrightness(targetValue)); + } + } else if (command instanceof RefreshType) { + pollForUpdate(); + } else { + logger.trace("UNKNOWN COMMAND: {} {}", command.getClass().toString(), channelUID); + } + }); + } + + @Override + protected void pollForDeviceData(final ExpiringCache cachedResponse) { + String response; + VeSyncV2BypassHumidifierStatus humidifierStatus; + synchronized (pollLock) { + response = cachedResponse.getValue(); + boolean cachedDataUsed = response != null; + if (response == null) { + logger.trace("Requesting fresh response"); + response = sendV2BypassCommand(DEVICE_GET_HUMIDIFIER_STATUS, + new VeSyncRequestManagedDeviceBypassV2.EmptyPayload()); + } else { + logger.trace("Using cached response {}", response); + } + + if (response.equals(EMPTY_STRING)) { + return; + } + + humidifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassHumidifierStatus.class); + + if (humidifierStatus == null) { + return; + } + + if (!cachedDataUsed) { + cachedResponse.putValue(response); + } + } + + // Bail and update the status of the thing - it will be updated to online by the next search + // that detects it is online. + if (humidifierStatus.isMsgDeviceOffline()) { + updateStatus(ThingStatus.OFFLINE); + return; + } else if (humidifierStatus.isMsgSuccess()) { + updateStatus(ThingStatus.ONLINE); + } + + if (!"0".equals(humidifierStatus.result.getCode())) { + logger.warn("Check correct Thing type has been set - API gave a unexpected response for an Air Humidifier"); + return; + } + + final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE); + + updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(humidifierStatus.result.result.enabled)); + updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(humidifierStatus.result.result.display)); + updateState(DEVICE_CHANNEL_WATER_LACKS, OnOffType.from(humidifierStatus.result.result.waterLacks)); + updateState(DEVICE_CHANNEL_HUMIDITY_HIGH, OnOffType.from(humidifierStatus.result.result.humidityHigh)); + updateState(DEVICE_CHANNEL_WATER_TANK_LIFTED, OnOffType.from(humidifierStatus.result.result.waterTankLifted)); + updateState(DEVICE_CHANNEL_STOP_AT_TARGET, + OnOffType.from(humidifierStatus.result.result.automaticStopReachTarget)); + updateState(DEVICE_CHANNEL_HUMIDITY, + new QuantityType<>(humidifierStatus.result.result.humidity, Units.PERCENT)); + updateState(DEVICE_CHANNEL_MIST_LEVEL, new DecimalType(humidifierStatus.result.result.mistLevel)); + updateState(DEVICE_CHANNEL_HUMIDIFIER_MODE, new StringType(humidifierStatus.result.result.mode)); + + // Only the 300S supports nightlight currently of tested devices. + if (DEV_TYPE_CLASSIC_300S.equals(deviceType) || DEV_TYPE_CORE_301S.equals(deviceType)) { + // Map the numeric that only applies to the same modes as the Air Filter 300S series. + if (humidifierStatus.result.result.nightLightBrightness == 0) { + updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_OFF)); + } else if (humidifierStatus.result.result.nightLightBrightness == 100) { + updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_ON)); + } else { + updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_DIM)); + } + } else if (DEV_TYPE_600S.equals(deviceType) || DEV_TYPE_600S_EU.equals(deviceType)) { + updateState(DEVICE_CHANNEL_WARM_ENABLED, OnOffType.from(humidifierStatus.result.result.warnEnabled)); + updateState(DEVICE_CHANNEL_WARM_LEVEL, new DecimalType(humidifierStatus.result.result.warmLevel)); + } + + updateState(DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY, + new QuantityType<>(humidifierStatus.result.result.configuration.autoTargetHumidity, Units.PERCENT)); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceAirPurifierHandler.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceAirPurifierHandler.java new file mode 100644 index 00000000000..2d02cc7ceb1 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceAirPurifierHandler.java @@ -0,0 +1,423 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handlers; + +import static org.openhab.binding.vesync.internal.VeSyncConstants.*; +import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestV1ManagedDeviceDetails; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassPurifierStatus; +import org.openhab.binding.vesync.internal.dto.responses.v1.VeSyncV1AirPurifierDeviceDetailsResponse; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.library.items.DateTimeItem; +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.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link VeSyncDeviceAirPurifierHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler { + + public static final int DEFAULT_AIR_PURIFIER_POLL_RATE = 120; + // "Device Type" values + public static final String DEV_TYPE_CORE_600S = "LAP-C601S-WUS"; + public static final String DEV_TYPE_CORE_400S = "Core400S"; + public static final String DEV_TYPE_CORE_300S = "Core300S"; + public static final String DEV_TYPE_CORE_201S = "LAP-C201S-AUSR"; + public static final String DEV_TYPE_CORE_200S = "Core200S"; + public static final String DEV_TYPE_LV_PUR131S = "LV-PUR131S"; + public static final List SUPPORTED_DEVICE_TYPES = Arrays.asList(DEV_TYPE_CORE_600S, DEV_TYPE_CORE_400S, + DEV_TYPE_CORE_300S, DEV_TYPE_CORE_201S, DEV_TYPE_CORE_200S, DEV_TYPE_LV_PUR131S); + + private static final List CORE_400S600S_FAN_MODES = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP); + private static final List CORE_200S300S_FAN_MODES = Arrays.asList(MODE_MANUAL, MODE_SLEEP); + private static final List CORE_200S300S_NIGHT_LIGHT_MODES = Arrays.asList(MODE_ON, MODE_DIM, MODE_OFF); + + private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirPurifierHandler.class); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIR_PURIFIER); + + private final Object pollLock = new Object(); + + public VeSyncDeviceAirPurifierHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + super.initialize(); + customiseChannels(); + } + + @Override + protected @NotNull String[] getChannelsToRemove() { + String[] toRemove = new String[] {}; + final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE); + if (deviceType != null) { + switch (deviceType) { + case DEV_TYPE_CORE_600S: + case DEV_TYPE_CORE_400S: + toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT }; + break; + case DEV_TYPE_LV_PUR131S: + toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT, DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE, + DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF, DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, + DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING, DEVICE_CHANNEL_AIRQUALITY_PM25, + DEVICE_CHANNEL_AF_SCHEDULES_COUNT, DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER }; + break; + default: + toRemove = new String[] { DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, DEVICE_CHANNEL_AF_SCHEDULES_COUNT }; + } + } + return toRemove; + } + + @Override + public void updateBridgeBasedPolls(final VeSyncBridgeConfiguration config) { + Integer pollRate = config.airPurifierPollInterval; + if (pollRate == null) { + pollRate = DEFAULT_AIR_PURIFIER_POLL_RATE; + } + + if (ThingStatus.OFFLINE.equals(getThing().getStatus())) { + setBackgroundPollInterval(-1); + } else { + setBackgroundPollInterval(pollRate); + } + } + + @Override + public void dispose() { + this.setBackgroundPollInterval(-1); + } + + @Override + protected boolean isDeviceSupported() { + final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE); + if (deviceType == null) { + return false; + } + return SUPPORTED_DEVICE_TYPES.contains(deviceType); + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE); + if (deviceType == null) { + return; + } + + scheduler.submit(() -> { + + if (command instanceof OnOffType) { + switch (channelUID.getId()) { + case DEVICE_CHANNEL_ENABLED: + sendV2BypassControlCommand(DEVICE_SET_SWITCH, + new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload(command.equals(OnOffType.ON), + 0)); + break; + case DEVICE_CHANNEL_DISPLAY_ENABLED: + sendV2BypassControlCommand(DEVICE_SET_DISPLAY, + new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON))); + break; + case DEVICE_CHANNEL_CHILD_LOCK_ENABLED: + sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK, + new VeSyncRequestManagedDeviceBypassV2.SetChildLock(command.equals(OnOffType.ON))); + break; + } + } else if (command instanceof StringType) { + switch (channelUID.getId()) { + case DEVICE_CHANNEL_FAN_MODE_ENABLED: + final String targetFanMode = command.toString().toLowerCase(); + switch (deviceType) { + case DEV_TYPE_CORE_600S: + case DEV_TYPE_CORE_400S: + if (!CORE_400S600S_FAN_MODES.contains(targetFanMode)) { + logger.warn( + "Fan mode command for \"{}\" is not valid in the (Core400S) API possible options {}", + command, String.join(",", CORE_400S600S_FAN_MODES)); + return; + } + break; + case DEV_TYPE_CORE_200S: + case DEV_TYPE_CORE_201S: + case DEV_TYPE_CORE_300S: + if (!CORE_200S300S_FAN_MODES.contains(targetFanMode)) { + logger.warn( + "Fan mode command for \"{}\" is not valid in the (Core200S/Core300S) API possible options {}", + command, String.join(",", CORE_200S300S_FAN_MODES)); + return; + } + break; + } + + sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetMode(targetFanMode)); + break; + case DEVICE_CHANNEL_AF_NIGHT_LIGHT: + final String targetNightLightMode = command.toString().toLowerCase(); + switch (deviceType) { + case DEV_TYPE_CORE_600S: + case DEV_TYPE_CORE_400S: + logger.warn("Core400S API does not support night light"); + return; + case DEV_TYPE_CORE_200S: + case DEV_TYPE_CORE_201S: + case DEV_TYPE_CORE_300S: + if (!CORE_200S300S_NIGHT_LIGHT_MODES.contains(targetNightLightMode)) { + logger.warn( + "Night light mode command for \"{}\" is not valid in the (Core200S/Core300S) API possible options {}", + command, String.join(",", CORE_200S300S_NIGHT_LIGHT_MODES)); + return; + } + + sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT, + new VeSyncRequestManagedDeviceBypassV2.SetNightLight(targetNightLightMode)); + + break; + } + break; + } + } else if (command instanceof QuantityType) { + switch (channelUID.getId()) { + case DEVICE_CHANNEL_FAN_SPEED_ENABLED: + // If the fan speed is being set enforce manual mode + sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false); + + int requestedLevel = ((QuantityType) command).intValue(); + if (requestedLevel < 1) { + logger.warn("Fan speed command less than 1 - adjusting to 1 as the valid API value"); + requestedLevel = 1; + } + + switch (deviceType) { + case DEV_TYPE_CORE_600S: + case DEV_TYPE_CORE_400S: + if (requestedLevel > 4) { + logger.warn( + "Fan speed command greater than 4 - adjusting to 4 as the valid (Core400S) API value"); + requestedLevel = 4; + } + break; + case DEV_TYPE_CORE_200S: + case DEV_TYPE_CORE_201S: + case DEV_TYPE_CORE_300S: + if (requestedLevel > 3) { + logger.warn( + "Fan speed command greater than 3 - adjusting to 3 as the valid (Core200S/Core300S) API value"); + requestedLevel = 3; + } + break; + } + + sendV2BypassControlCommand(DEVICE_SET_LEVEL, + new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_WIND, + requestedLevel)); + break; + } + } else if (command instanceof RefreshType) { + pollForUpdate(); + } else { + logger.trace("UNKNOWN COMMAND: {} {}", command.getClass().toString(), channelUID); + } + }); + } + + @Override + protected void pollForDeviceData(final ExpiringCache cachedResponse) { + final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE); + if (deviceType == null) { + return; + } + + switch (deviceType) { + case DEV_TYPE_CORE_600S: + case DEV_TYPE_CORE_400S: + case DEV_TYPE_CORE_300S: + case DEV_TYPE_CORE_201S: + case DEV_TYPE_CORE_200S: + processV2BypassPoll(cachedResponse); + break; + case DEV_TYPE_LV_PUR131S: + processV1AirPurifierPoll(cachedResponse); + break; + } + } + + private void processV1AirPurifierPoll(final ExpiringCache cachedResponse) { + final String deviceUuid = getThing().getProperties().get(DEVICE_PROP_DEVICE_UUID); + if (deviceUuid == null) { + return; + } + + String response; + VeSyncV1AirPurifierDeviceDetailsResponse purifierStatus; + synchronized (pollLock) { + response = cachedResponse.getValue(); + boolean cachedDataUsed = response != null; + if (response == null) { + logger.trace("Requesting fresh response"); + response = sendV1Command("POST", "https://smartapi.vesync.com/131airPurifier/v1/device/deviceDetail", + new VeSyncRequestV1ManagedDeviceDetails(deviceUuid)); + } else { + logger.trace("Using cached response {}", response); + } + + if (response.equals(EMPTY_STRING)) { + return; + } + + purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV1AirPurifierDeviceDetailsResponse.class); + + if (purifierStatus == null) { + return; + } + + if (!cachedDataUsed) { + cachedResponse.putValue(response); + } + } + + // Bail and update the status of the thing - it will be updated to online by the next search + // that detects it is online. + if (purifierStatus.isDeviceOnline()) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE); + return; + } + + if (!"0".equals(purifierStatus.getCode())) { + logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier"); + return; + } + + updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(MODE_ON.equals(purifierStatus.getDeviceStatus()))); + updateState(DEVICE_CHANNEL_CHILD_LOCK_ENABLED, OnOffType.from(MODE_ON.equals(purifierStatus.getChildLock()))); + updateState(DEVICE_CHANNEL_FAN_MODE_ENABLED, new StringType(purifierStatus.getMode())); + updateState(DEVICE_CHANNEL_FAN_SPEED_ENABLED, new DecimalType(String.valueOf(purifierStatus.getLevel()))); + updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(MODE_ON.equals(purifierStatus.getScreenStatus()))); + updateState(DEVICE_CHANNEL_AIRQUALITY_BASIC, new DecimalType(purifierStatus.getAirQuality())); + } + + private void processV2BypassPoll(final ExpiringCache cachedResponse) { + String response; + VeSyncV2BypassPurifierStatus purifierStatus; + synchronized (pollLock) { + response = cachedResponse.getValue(); + boolean cachedDataUsed = response != null; + if (response == null) { + logger.trace("Requesting fresh response"); + response = sendV2BypassCommand(DEVICE_GET_PURIFIER_STATUS, + new VeSyncRequestManagedDeviceBypassV2.EmptyPayload()); + } else { + logger.trace("Using cached response {}", response); + } + + if (response.equals(EMPTY_STRING)) { + return; + } + + purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassPurifierStatus.class); + + if (purifierStatus == null) { + return; + } + + if (!cachedDataUsed) { + cachedResponse.putValue(response); + } + } + + // Bail and update the status of the thing - it will be updated to online by the next search + // that detects it is online. + if (purifierStatus.isMsgDeviceOffline()) { + updateStatus(ThingStatus.OFFLINE); + return; + } else if (purifierStatus.isMsgSuccess()) { + updateStatus(ThingStatus.ONLINE); + } + + if (!"0".equals(purifierStatus.result.getCode())) { + logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier"); + return; + } + + updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(purifierStatus.result.result.enabled)); + updateState(DEVICE_CHANNEL_CHILD_LOCK_ENABLED, OnOffType.from(purifierStatus.result.result.childLock)); + updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(purifierStatus.result.result.display)); + updateState(DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING, + new QuantityType<>(purifierStatus.result.result.filterLife, Units.PERCENT)); + updateState(DEVICE_CHANNEL_FAN_MODE_ENABLED, new StringType(purifierStatus.result.result.mode)); + updateState(DEVICE_CHANNEL_FAN_SPEED_ENABLED, new DecimalType(purifierStatus.result.result.level)); + updateState(DEVICE_CHANNEL_ERROR_CODE, new DecimalType(purifierStatus.result.result.deviceErrorCode)); + updateState(DEVICE_CHANNEL_AIRQUALITY_BASIC, new DecimalType(purifierStatus.result.result.airQuality)); + updateState(DEVICE_CHANNEL_AIRQUALITY_PM25, + new QuantityType<>(purifierStatus.result.result.airQualityValue, Units.MICROGRAM_PER_CUBICMETRE)); + + updateState(DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER, + OnOffType.from(purifierStatus.result.result.configuration.displayForever)); + + updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF, + new StringType(purifierStatus.result.result.configuration.autoPreference.autoType)); + + updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE, + new DecimalType(purifierStatus.result.result.configuration.autoPreference.roomSize)); + + // Only 400S appears to have this JSON extension object + if (purifierStatus.result.result.extension != null) { + if (purifierStatus.result.result.extension.timerRemain > 0) { + updateState(DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, new DateTimeType(LocalDateTime.now() + .plus(purifierStatus.result.result.extension.timerRemain, ChronoUnit.SECONDS).toString())); + } else { + updateState(DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, new DateTimeItem("nullEnforcements").getState()); + } + updateState(DEVICE_CHANNEL_AF_SCHEDULES_COUNT, + new DecimalType(purifierStatus.result.result.extension.scheduleCount)); + } + + // Not applicable to 400S payload's + if (purifierStatus.result.result.nightLight != null) { + updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new DecimalType(purifierStatus.result.result.nightLight)); + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 00000000000..98c9e1c4082 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + VeSync Binding + This is the binding for the VeSync products. Currently, this supports the Levoit branded Air Purifiers and + Humidifiers using the v2 protocol. + + diff --git a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..e2b841d63f1 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,334 @@ + + + + + + The VeSync bridge represents the VeSync cloud service. + + + + + + + + + + email + true + + Name of a registered VeSync user, that allows to access the mobile application. + + + password + true + + Password for the registered VeSync username, that allows to access the mobile application. + + + + Enable background scanning for new devices. + true + + + + Seconds between fetching background updates about the air purifiers / humidifiers. + 60 + + + + + + + + + + + A Air Purifier uplinking to VeSync + + + + + + + + + + + + + + + + + + + + + + + + + macId + + + + + The MAC Id of the device as reported by the API. + + + + The name allocated to the device by the app. (Must be unique if used) + + + + + + + + + + + + A Air Humidifier uplinking to VeSync + + + + + + + + + + + + + + + + + + + + + + + macId + + + + + The MAC Id of the device as reported by the API. + + + + The name allocated to the device by the app. (Must be unique if used) + + + + + + + Switch + + Indicator if the device is switched on + + + + + Switch + + Indicator if the devices child lock is enabled (Display Lock) + + + + + Switch + + Indicator if the devices display is enabled + + + + + Number:Dimensionless + + Indicator of the remaining filter life + + + + + String + + The operating mode the air purifier is currently set to + + + + + + + + + + + String + + The operating mode of the night light functionality + + + + + + + + + + + + Number:Dimensionless + + Indicator of the current fan speed + + + + + Number:Dimensionless + + Indicator of the current error code of the device + + + + + Number:Dimensionless + + System representation of air quality + + + + + Number:Density + + Indicator of current air quality + + + + + Switch + + Configuration: If the devices display is enabled forever + + + + String + + The operating mode when the air purifier is set to auto + + + + + + + + + + + DateTime + + The time when the auto off timer will be reached + + + + + Number:Dimensionless + + Room size (foot sq) for efficient auto mode + + + + + Number:Dimensionless + + The current number of schedules configured + + + + + + Switch + + Indicator if the devices water is low or empty + + + + + Switch + + Indicator if the device is measuring high humidity + + + + + Switch + + Indicator if the device is reporting the water tank as removed + + + + + Switch + + Indicator if the device is set to stop when the humidity set point has been reached + + + + + Number:Dimensionless + + System representation of humidity + + + + + Number:Dimensionless + + Humidity Set Point + + + + + Number:Dimensionless + + System representation of mist level + + + + + String + + The operating mode the air humidifier is currently set to + + + + + + + + + + + Switch + + Indicator if the device is set to warm mist + + + + + Number:Dimensionless + + Warm Level + + + + + diff --git a/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncAuthenticatedRequestTest.java b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncAuthenticatedRequestTest.java new file mode 100644 index 00000000000..9084ae88067 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncAuthenticatedRequestTest.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handler.requests; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.Test; +import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.requests.VesyncAuthenticatedRequest; +import org.openhab.binding.vesync.internal.dto.requests.VesyncLoginCredentials; +import org.openhab.binding.vesync.internal.dto.responses.VesyncLoginResponse; + +/** + * The {@link VesyncLoginCredentials} class implements unit test case for {@link VesyncLoginResponse} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VesyncAuthenticatedRequestTest { + + public final static VesyncLoginResponse.VesyncUserSession testUser = VeSyncConstants.GSON.fromJson( + org.openhab.binding.vesync.internal.handler.responses.VesyncLoginResponseTest.testGoodLoginResponseBody, + VesyncLoginResponse.class).result; + + @Test + public void checkBaseFieldsExist() { + String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5")); + + assertEquals(true, content.contains("\"timeZone\": \"America/New_York\"")); + assertEquals(true, content.contains("\"acceptLanguage\": \"en\"")); + + assertEquals(true, content.contains("\"appVersion\": \"2.5.1\"")); + assertEquals(true, content.contains("\"phoneBrand\": \"SM N9005\"")); + assertEquals(true, content.contains("\"phoneOS\": \"Android\"")); + + Pattern p = Pattern.compile("\"traceId\": \"\\d+\""); + Matcher m = p.matcher(content); + assertEquals(true, m.find()); + } + + @Test + public void checkAuthenicationData() { + + // Simulate as the code flow should run - parse data and then use it + VesyncLoginResponse response = VeSyncConstants.GSON + .fromJson(org.openhab.binding.vesync.internal.handler.responses.VesyncLoginResponseTest.testGoodLoginResponseBody, VesyncLoginResponse.class); + + String content = ""; + + try { + content = VeSyncConstants.GSON.toJson(new VesyncAuthenticatedRequest(response.result)); + } catch (AuthenticationException ae) { + + } + + assertEquals(true, content.contains("\"token\": \"AccessTokenString=\"")); + assertEquals(true, content.contains("\"accountID\": \"5328043\"")); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncLoginCredentialsTest.java b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncLoginCredentialsTest.java new file mode 100644 index 00000000000..a670f91484f --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncLoginCredentialsTest.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handler.requests; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.Test; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.api.VesyncV2ApiHelper; +import org.openhab.binding.vesync.internal.dto.requests.VesyncLoginCredentials; + +/** + * The {@link VesyncLoginCredentials} class implements unit test case for {@link VesyncLoginResponse} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VesyncLoginCredentialsTest { + + @Test + public void checkMd5Calculation() { + assertEquals("577441848f056cd02d4c500b25fdd76a",VesyncV2ApiHelper.calculateMd5("TestHashInPythonLib=+")); + } + + @Test + public void checkBaseFieldsExist() { + String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5")); + + assertEquals(true, content.contains("\"timeZone\": \"America/New_York\"")); + assertEquals(true, content.contains("\"acceptLanguage\": \"en\"")); + + assertEquals(true, content.contains("\"appVersion\": \"2.5.1\"")); + assertEquals(true, content.contains("\"phoneBrand\": \"SM N9005\"")); + assertEquals(true, content.contains("\"phoneOS\": \"Android\"")); + + Pattern p = Pattern.compile("\"traceId\": \"\\d+\""); + Matcher m = p.matcher(content); + assertEquals(true, m.find()); + } + + @Test + public void checkLoginMethodJson() { + + String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5")); + + assertEquals(true, content.contains("\"method\": \"login\"")); + assertEquals(true, content.contains("\"email\": \"username\"")); + assertEquals(true, content.contains("\"password\": \"passmd5\"")); + assertEquals(true, content.contains("\"userType\": \"1\"")); + assertEquals(true, content.contains("\"devToken\": \"\"")); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestManagedDeviceBypassV2Test.java b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestManagedDeviceBypassV2Test.java new file mode 100644 index 00000000000..91bf977446b --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestManagedDeviceBypassV2Test.java @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handler.requests; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.Test; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.api.VesyncV2ApiHelper; +import org.openhab.binding.vesync.internal.dto.requests.VesyncLoginCredentials; +import org.openhab.binding.vesync.internal.dto.requests.VesyncRequestManagedDeviceBypassV2; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The {@link VesyncLoginCredentials} class implements unit test case for {@link VesyncLoginResponse} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VesyncRequestManagedDeviceBypassV2Test { + + @Test + public void checkMd5Calculation() { + assertEquals("577441848f056cd02d4c500b25fdd76a",VesyncV2ApiHelper.calculateMd5("TestHashInPythonLib=+")); + } + + @Test + public void checkBaseFieldsExist() { + String content = VeSyncConstants.GSON.toJson(new VesyncRequestManagedDeviceBypassV2()); + + assertEquals(true, content.contains("\"method\": \"bypassV2\"")); + assertEquals(true, content.contains("\"data\": {}")); + } + + @Test + public void checkEmptyPayload() { + final VesyncRequestManagedDeviceBypassV2.EmptyPayload testPaylaod = new VesyncRequestManagedDeviceBypassV2.EmptyPayload(); + final String contentTest1 = VeSyncConstants.GSON.toJson(testPaylaod); + assertEquals(true, contentTest1.equals("{}")); + } + + @Test + public void checkSetLevelPayload() { + final VesyncRequestManagedDeviceBypassV2.SetLevelPayload testPaylaod = new VesyncRequestManagedDeviceBypassV2.SetLevelPayload(1,"stringval",2); + final String contentTest1 = VeSyncConstants.GSON.toJson(testPaylaod); + assertEquals(true, contentTest1.contains("\"id\": 1")); + assertEquals(true,contentTest1.contains("\"type\": \"stringval\"")); + assertEquals(true,contentTest1.contains("\"level\": 2")); + } + + @Test + public void checkSetChildLockPayload() { + final VesyncRequestManagedDeviceBypassV2.SetChildLock testPaylaod = new VesyncRequestManagedDeviceBypassV2.SetChildLock(false); + final String contentTest1 = VeSyncConstants.GSON.toJson(testPaylaod); + assertEquals(true,contentTest1.contains("\"child_lock\": false")); + + testPaylaod.childLock = true; + final String contentTest2 = VeSyncConstants.GSON.toJson(testPaylaod); + assertEquals(true,contentTest2.contains("\"child_lock\": true")); + } + + @Test + public void checkSetSwitchPayload() { + final VesyncRequestManagedDeviceBypassV2.SetSwitchPayload testPaylaod = new VesyncRequestManagedDeviceBypassV2.SetSwitchPayload(true,0); + final String contentTest1 = VeSyncConstants.GSON.toJson(testPaylaod); + assertEquals(true, contentTest1.contains("\"enabled\": true")); + assertEquals(true, contentTest1.contains("\"id\": 0")); + + testPaylaod.enabled = false; + testPaylaod.id = 100; + final String contentTest2 = VeSyncConstants.GSON.toJson(testPaylaod); + assertEquals(true, contentTest2.contains("\"enabled\": false")); + assertEquals(true, contentTest2.contains("\"id\": 100")); + } + + @Test + public void checkSetNightLightPayload() { + final VesyncRequestManagedDeviceBypassV2.SetNightLight testPaylaod = new VesyncRequestManagedDeviceBypassV2.SetNightLight("myValue"); + final String contentTest1 = VeSyncConstants.GSON.toJson(testPaylaod); + assertEquals(true, contentTest1.contains("\"night_light\": \"myValue\"")); + } + + @Test + public void checkSetTargetHumidityPayload() { + final VesyncRequestManagedDeviceBypassV2.SetTargetHumidity test0Paylaod = new VesyncRequestManagedDeviceBypassV2.SetTargetHumidity(0); + final String contentTest1 = VeSyncConstants.GSON.toJson(test0Paylaod); + assertEquals(true, contentTest1.contains("\"target_humidity\": 0")); + + final VesyncRequestManagedDeviceBypassV2.SetTargetHumidity test100Paylaod = new VesyncRequestManagedDeviceBypassV2.SetTargetHumidity(100); + final String contentTest2 = VeSyncConstants.GSON.toJson(test100Paylaod); + assertEquals(true, contentTest2.contains("\"target_humidity\": 100")); + } + + @Test + public void checkSetNightLightBrightnessPayload() { + final VesyncRequestManagedDeviceBypassV2.SetNightLightBrightness test0Paylaod = new VesyncRequestManagedDeviceBypassV2.SetNightLightBrightness(0); + final String contentTest1 = VeSyncConstants.GSON.toJson(test0Paylaod); + assertEquals(true, contentTest1.contains("\"night_light_brightness\": 0")); + + final VesyncRequestManagedDeviceBypassV2.SetNightLightBrightness test100Paylaod = new VesyncRequestManagedDeviceBypassV2.SetNightLightBrightness(100); + final String contentTest2 = VeSyncConstants.GSON.toJson(test100Paylaod); + assertEquals(true, contentTest2.contains("\"night_light_brightness\": 100")); + } + + @Test + public void checkEnabledPayload() { + final VesyncRequestManagedDeviceBypassV2.EnabledPayload enabledOn = new VesyncRequestManagedDeviceBypassV2.EnabledPayload(true); + final String contentTest1 = VeSyncConstants.GSON.toJson(enabledOn); + assertEquals(true, contentTest1.contains("\"enabled\": true")); + + final VesyncRequestManagedDeviceBypassV2.EnabledPayload enabledOff = new VesyncRequestManagedDeviceBypassV2.EnabledPayload(false); + final String contentTest2 = VeSyncConstants.GSON.toJson(enabledOff); + assertEquals(true, contentTest2.contains("\"enabled\": false")); + } + + @Test + public void checkLoginMethodJson() { + + String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5")); + + assertEquals(true, content.contains("\"method\": \"login\"")); + assertEquals(true, content.contains("\"email\": \"username\"")); + assertEquals(true, content.contains("\"password\": \"passmd5\"")); + assertEquals(true, content.contains("\"userType\": \"1\"")); + assertEquals(true, content.contains("\"devToken\": \"\"")); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestManagedDevicesPageTest.java b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestManagedDevicesPageTest.java new file mode 100644 index 00000000000..3263ceffe18 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestManagedDevicesPageTest.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handler.requests; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.Test; +import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.requests.VesyncLoginCredentials; +import org.openhab.binding.vesync.internal.dto.requests.VesyncRequestManagedDevicesPage; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The {@link VesyncRequestManagedDevicesPageTest} class implements unit test case for + * {@link VesyncRequestManagedDevicesPage} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VesyncRequestManagedDevicesPageTest { + + @Test + public void checkBaseFieldsExist() { + String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5")); + + assertEquals(true, content.contains("\"timeZone\": \"America/New_York\"")); + assertEquals(true, content.contains("\"acceptLanguage\": \"en\"")); + + assertEquals(true, content.contains("\"appVersion\": \"2.5.1\"")); + assertEquals(true, content.contains("\"phoneBrand\": \"SM N9005\"")); + assertEquals(true, content.contains("\"phoneOS\": \"Android\"")); + + Pattern p = Pattern.compile("\"traceId\": \"\\d+\""); + Matcher m = p.matcher(content); + assertEquals(true, m.find()); + } + + @Test + public void checkRequestDevicesFields() { + + String content = ""; + try { + content = VeSyncConstants.GSON + .toJson(new VesyncRequestManagedDevicesPage(org.openhab.binding.vesync.internal.handler.requests.VesyncAuthenticatedRequestTest.testUser, 1, 100)); + } catch (AuthenticationException ae) { + + } + + + assertEquals(true, content.contains("\"method\": \"devices\"")); + assertEquals(true, content.contains("\"pageNo\": \"1\"")); + assertEquals(true, content.contains("\"pageSize\": \"100\"")); + assertEquals(true, content.contains("\"token\": \"AccessTokenString=\"")); + assertEquals(true, content.contains("\"accountID\": \"5328043\"")); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestTest.java b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestTest.java new file mode 100644 index 00000000000..c72fc20ac2e --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestTest.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package java.org.openhab.binding.vesync.internal.handler.requests; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.Test; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.requests.VesyncRequest; + +/** + * The {@link VesyncRequestTest} class implements unit test case for {@link VesyncRequest} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VesyncRequestTest { + + @Test + public void checkBaseFieldsExist() { + String content = VeSyncConstants.GSON.toJson(new VesyncRequest()); + + assertEquals(true, content.contains("\"timeZone\": \"America/New_York\"")); + assertEquals(true, content.contains("\"acceptLanguage\": \"en\"")); + + assertEquals(true, content.contains("\"appVersion\": \"2.5.1\"")); + assertEquals(true, content.contains("\"phoneBrand\": \"SM N9005\"")); + assertEquals(true, content.contains("\"phoneOS\": \"Android\"")); + + Pattern p = Pattern.compile("\"traceId\": \"\\d+\""); + Matcher m = p.matcher(content); + assertEquals(true, m.find()); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestV1ManagedDeviceDetailsTest.java b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestV1ManagedDeviceDetailsTest.java new file mode 100644 index 00000000000..420fc4b1958 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/requests/VesyncRequestV1ManagedDeviceDetailsTest.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handler.requests; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.Test; +import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.requests.VesyncLoginCredentials; +import org.openhab.binding.vesync.internal.dto.requests.VesyncRequestManagedDevicesPage; +import org.openhab.binding.vesync.internal.dto.requests.VesyncRequestV1ManagedDeviceDetails; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The {@link VesyncRequestV1ManagedDeviceDetails} class implements unit test case for + * {@link VesyncRequestManagedDevicesPage} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VesyncRequestV1ManagedDeviceDetailsTest { + + // Verified content URLS + // https://smartapi.vesync.com/131airPurifier/v1/device/deviceDetail + + @Test + public void checkBaseFieldsExist() { + String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5")); + + assertEquals(true, content.contains("\"timeZone\": \"America/New_York\"")); + assertEquals(true, content.contains("\"acceptLanguage\": \"en\"")); + + assertEquals(true, content.contains("\"appVersion\": \"2.5.1\"")); + assertEquals(true, content.contains("\"phoneBrand\": \"SM N9005\"")); + assertEquals(true, content.contains("\"phoneOS\": \"Android\"")); + + Pattern p = Pattern.compile("\"traceId\": \"\\d+\""); + Matcher m = p.matcher(content); + assertEquals(true, m.find()); + } + + @Test + public void checkRequestDevicesFields() { + + String content = ""; + try { + content = VeSyncConstants.GSON + .toJson(new VesyncRequestV1ManagedDeviceDetails(VesyncAuthenticatedRequestTest.testUser, "MyDeviceUUID")); + } catch (AuthenticationException ae) { + + } + + assertEquals(true, content.contains("\"uuid\": \"MyDeviceUUID\"")); + assertEquals(true, content.contains("\"mobileId\": \"1234567890123456\"")); + assertEquals(true, content.contains("\"token\": \"AccessTokenString=\"")); + assertEquals(true, content.contains("\"accountID\": \"5328043\"")); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/VesyncLoginResponseTest.java b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/VesyncLoginResponseTest.java new file mode 100644 index 00000000000..5523c10f371 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/VesyncLoginResponseTest.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handler.responses; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.responses.VesyncLoginResponse; + +/** + * The {@link VesyncLoginResponseTest} class implements unit test case for {@link VesyncLoginResponse} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VesyncLoginResponseTest { + + public final static String testGoodLoginResponseBody = "{\r\n" + " \"traceId\": \"1634253816\",\r\n" + + " \"code\": 0,\r\n" + " \"msg\": \"request success\",\r\n" + " \"result\": {\r\n" + + " \"isRequiredVerify\": true,\r\n" + " \"accountID\": \"5328043\",\r\n" + + " \"avatarIcon\": \"https://image.vesync.com/defaultImages/user/avatar_nor.png\",\r\n" + + " \"birthday\": \"\",\r\n" + " \"gender\": \"\",\r\n" + + " \"acceptLanguage\": \"en\",\r\n" + " \"userType\": \"1\",\r\n" + + " \"nickName\": \"david.goodyear\",\r\n" + " \"mailConfirmation\": true,\r\n" + + " \"termsStatus\": true,\r\n" + " \"gdprStatus\": true,\r\n" + + " \"countryCode\": \"GB\",\r\n" + " \"registerAppVersion\": \"VeSync 3.1.37 build3\",\r\n" + + " \"registerTime\": \"2021-10-14 17:35:50\",\r\n" + + " \"verifyEmail\": \"david.goodyear@gmail.com\",\r\n" + " \"heightCm\": 0.0,\r\n" + + " \"weightTargetSt\": 0.0,\r\n" + " \"heightUnit\": \"FT\",\r\n" + + " \"heightFt\": 0.0,\r\n" + " \"weightTargetKg\": 0.0,\r\n" + + " \"weightTargetLb\": 0.0,\r\n" + " \"weightUnit\": \"LB\",\r\n" + + " \"targetBfr\": 0.0,\r\n" + " \"displayFlag\": [],\r\n" + + " \"real_weight_kg\": 0.0,\r\n" + " \"real_weight_lb\": 0.0,\r\n" + + " \"real_weight_unit\": \"lb\",\r\n" + " \"heart_rate_zones\": 0.0,\r\n" + + " \"run_step_long_cm\": 0.0,\r\n" + " \"walk_step_long_cm\": 0.0,\r\n" + + " \"step_target\": 0.0,\r\n" + " \"sleep_target_mins\": 0.0,\r\n" + + " \"token\": \"AccessTokenString=\"\r\n" + " }\r\n" + "}"; + + @Test + public void testParseLoginGoodResponse() { + VesyncLoginResponse response = VeSyncConstants.GSON.fromJson(testGoodLoginResponseBody, + VesyncLoginResponse.class); + if (response != null) { + assertEquals("1634253816", response.getTraceId()); + assertEquals("AccessTokenString=", response.result.token); + assertEquals("request success", response.msg); + assertEquals("5328043", response.result.accountId); + assertEquals("VeSync 3.1.37 build3", response.result.registerAppVersion); + assertEquals("GB", response.result.countryCode); + assertTrue(response.isMsgSuccess()); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testParseLoginFailResponse() { + String testReponse = "{\r\n" + " \"traceId\": \"1634253816\",\r\n" + " \"code\": -11201022,\r\n" + + " \"msg\": \"password incorrect\",\r\n" + " \"result\": null\r\n" + "}"; + VesyncLoginResponse response = VeSyncConstants.GSON.fromJson(testReponse, + VesyncLoginResponse.class); + if (response != null) { + assertEquals("password incorrect", response.msg); + assertFalse(response.isMsgSuccess()); + } else { + fail("GSON returned null"); + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/VesyncManagedDevicesPageTest.java b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/VesyncManagedDevicesPageTest.java new file mode 100644 index 00000000000..0b5f0ada9c4 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/VesyncManagedDevicesPageTest.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handler.responses; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.responses.VesyncLoginResponse; +import org.openhab.binding.vesync.internal.dto.responses.VesyncManagedDevicesPage; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * The {@link VesyncManagedDevicesPageTest} class implements unit test case for {@link VesyncLoginResponse} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VesyncManagedDevicesPageTest { + + public final static String testGoodSearchResponsePageBody = "{\n" + + " \"traceId\": \"1634387642\",\n" + + " \"code\": 0,\n" + + " \"msg\": \"request success\",\n" + + " \"result\": {\n" + + " \"total\": 2,\n" + + " \"pageSize\": 100,\n" + + " \"pageNo\": 1,\n" + + " \"list\": [\n" + + " {\n" + + " \"deviceRegion\": \"EU\",\n" + + " \"isOwner\": true,\n" + + " \"authKey\": null,\n" + + " \"deviceName\": \"Air Filter\",\n" + + " \"deviceImg\": \"https://image.vesync.com/defaultImages/Core_400S_Series/icon_core400s_purifier_160.png\",\n" + + " \"cid\": \"cidValue1\",\n" + + " \"deviceStatus\": \"on\",\n" + + " \"connectionStatus\": \"online\",\n" + + " \"connectionType\": \"WiFi+BTOnboarding+BTNotify\",\n" + + " \"deviceType\": \"Core400S\",\n" + + " \"type\": \"wifi-air\",\n" + + " \"uuid\": \"abcdefab-1234-1234-abcd-123498761234\",\n" + + " \"configModule\": \"WiFiBTOnboardingNotify_AirPurifier_Core400S_EU\",\n" + + " \"macID\": \"ab:cd:ef:12:34:56\",\n" + + " \"mode\": \"simModeData\",\n" + + " \"speed\": 4,\n" + + " \"extension\": {\n" + + " \"airQuality\": -1,\n" + + " \"airQualityLevel\": 1,\n" + + " \"mode\": \"auto\",\n" + + " \"fanSpeedLevel\": \"1\"\n" + + " },\n" + + " \"currentFirmVersion\": null,\n" + + " \"subDeviceNo\": \"simSubDevice\",\n" + + " \"subDeviceType\": \"simSubDeviceType\",\n" + + " \"deviceFirstSetupTime\": \"Oct 15, 2021 3:43:02 PM\"\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + + @Test + public void testParseManagedDevicesSearchGoodResponse() { + VesyncManagedDevicesPage response = VeSyncConstants.GSON.fromJson(testGoodSearchResponsePageBody, + VesyncManagedDevicesPage.class); + if (response != null) { + assertEquals("1634387642", response.getTraceId()); + assertEquals("1", response.result.getPageNo()); + assertEquals("100", response.result.getPageSize()); + assertEquals("2", response.result.getTotal()); + assertEquals("1", String.valueOf(response.result.list.length)); + + assertEquals("EU", response.result.list[0].getDeviceRegion()); + assertEquals("Air Filter", response.result.list[0].getDeviceName()); + assertEquals("https://image.vesync.com/defaultImages/Core_400S_Series/icon_core400s_purifier_160.png", response.result.list[0].getDeviceImg()); + assertEquals("on", response.result.list[0].getDeviceStatus()); + assertEquals("online", response.result.list[0].getConnectionStatus()); + assertEquals("WiFi+BTOnboarding+BTNotify", response.result.list[0].getConnectionType()); + assertEquals("Core400S", response.result.list[0].getDeviceType()); + assertEquals("wifi-air", response.result.list[0].getType()); + assertEquals("abcdefab-1234-1234-abcd-123498761234", response.result.list[0].getUuid()); + assertEquals("WiFiBTOnboardingNotify_AirPurifier_Core400S_EU", response.result.list[0].getConfigModule()); + assertEquals("simModeData",response.result.list[0].getMode()); + assertEquals("simSubDevice", response.result.list[0].getSubDeviceNo()); + assertEquals("simSubDeviceType", response.result.list[0].getSubDeviceType()); + assertEquals( "4", response.result.list[0].getSpeed()); + assertEquals("cidValue1",response.result.list[0].getCid()); + assertEquals("ab:cd:ef:12:34:56", response.result.list[0].getMacId()); + } else { + fail("GSON returned null"); + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/VesyncResponseTest.java b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/VesyncResponseTest.java new file mode 100644 index 00000000000..caee4f9bf70 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/VesyncResponseTest.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handler.responses; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.Test; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.responses.VesyncResponse; + +/** + * The {@link VesyncResponseTest} class implements unit test case for {@link VesyncResponse} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VesyncResponseTest { + + @Test + public void checkBaseFields() { + String baseTestResponse = "{\"traceId\":\"1234569876\",\r\n\"code\": 142,\r\n\"msg\": \"Response Text\"\r\n}"; + VesyncResponse response = VeSyncConstants.GSON.fromJson(baseTestResponse, VesyncResponse.class); + if (response != null) { + assertEquals("1234569876", response.getTraceId()); + assertEquals("142", response.getCode()); + assertEquals("Response Text", response.msg); + assertEquals(false, response.isMsgSuccess()); + } else { + fail("GSON returned null"); + } + } + + @Test + public void checkResponseSuccessMsg() { + String baseTestResponse = "{\"traceId\":\"1234569876\",\r\n\"code\": 142,\r\n\"msg\": \"request success\"\r\n}"; + VesyncResponse response = VeSyncConstants.GSON.fromJson(baseTestResponse, VesyncResponse.class); + if (response != null) { + assertEquals(true, response.isMsgSuccess()); + } else { + fail("GSON returned null"); + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/v1/VesyncV1AirPurifierDeviceDetailsTest.java b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/v1/VesyncV1AirPurifierDeviceDetailsTest.java new file mode 100644 index 00000000000..793a990afc4 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/test/org/openhab/binding/vesync/internal/handler/responses/v1/VesyncV1AirPurifierDeviceDetailsTest.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handler.responses.v1; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.vesync.internal.VeSyncConstants; +import org.openhab.binding.vesync.internal.dto.responses.VesyncLoginResponse; +import org.openhab.binding.vesync.internal.dto.responses.v1.VesyncV1AirPurifierDeviceDetailsResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * The {@link VesyncV1AirPurifierDeviceDetailsTest} class implements unit test case for {@link VesyncLoginResponse} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VesyncV1AirPurifierDeviceDetailsTest { + + public final static String testAirPurifierResponseBasedOnCore400S = "{\n" + + " \"code\": 0,\n" + + " \"msg\": \"request success\",\n" + + " \"traceId\": \"1634255391\",\n" + + " \"screenStatus\": \"on1\",\n" + + " \"airQuality\": 1,\n" + + " \"level\": 2,\n" + + " \"mode\": \"manual\",\n" + + " \"deviceName\": \"Lounge Air Purifier\",\n" + + " \"currentFirmVersion\": \"1.0.17\",\n" + + " \"childLock\": \"off1\",\n" + + " \"deviceStatus\": \"on2\",\n" + + " \"deviceImg\": \"https://image.vesync.com/defaultImages/Core_400S_Series/icon_core400s_purifier_160.png\",\n" + + " \"connectionStatus\": \"online\"\n" + + "}"; + + @Test + public void testParseV1AirPurifierDeviceDetailsResponse() { + VesyncV1AirPurifierDeviceDetailsResponse response = VeSyncConstants.GSON.fromJson(testAirPurifierResponseBasedOnCore400S, + VesyncV1AirPurifierDeviceDetailsResponse.class); + + if (response != null) { + assertEquals("on1", response.getScreenStatus()); + assertEquals(1, response.getAirQuality()); + assertEquals(2, response.getLevel()); + assertEquals("manual", response.getMode()); + assertEquals("Lounge Air Purifier", response.getDeviceName()); + assertEquals("1.0.17", response.getCurrentFirmVersion()); + assertEquals("off1", response.getChildLock()); + assertEquals("on2", response.getDeviceStatus()); + assertEquals("https://image.vesync.com/defaultImages/Core_400S_Series/icon_core400s_purifier_160.png", response.getDeviceImgUrl()); + assertEquals("online", response.getConnectionStatus()); + } else { + fail("GSON returned null"); + } + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 2e778a98d4e..c2274f6475a 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -370,6 +370,7 @@ org.openhab.binding.venstarthermostat org.openhab.binding.ventaair org.openhab.binding.verisure + org.openhab.binding.vesync org.openhab.binding.vigicrues org.openhab.binding.vitotronic org.openhab.binding.volvooncall