From 239b3d3ff0a6150750a062781131b6b80d62f988 Mon Sep 17 00:00:00 2001 From: David Goodyear Date: Sun, 23 Jul 2023 15:49:56 +0100 Subject: [PATCH] [veSync] Device support enhancements Device support enhancements Signed-off-by: David Goodyear --- bundles/org.openhab.binding.vesync/README.md | 198 +++++++--- .../vesync/internal/VeSyncConstants.java | 4 +- .../internal/api/VeSyncV2ApiHelper.java | 5 +- .../discovery/VeSyncDiscoveryService.java | 3 + .../dto/requests/VeSyncProtocolConstants.java | 9 +- .../internal/dto/requests/VeSyncRequest.java | 5 + .../VeSyncRequestManagedDeviceBypassV2.java | 69 ++++ .../dto/requests/VeSyncRequestV1Command.java | 45 +++ .../VeSyncRequestV1ManagedDeviceDetails.java | 3 +- .../dto/requests/VeSyncRequestV1SetLevel.java | 36 ++ .../dto/requests/VeSyncRequestV1SetMode.java | 36 ++ .../requests/VeSyncRequestV1SetStatus.java | 36 ++ .../VeSyncV2BypassPurifierStatus.java | 3 +- .../VeSyncV2Ver2BypassHumidifierStatus.java | 109 ++++++ .../VeSyncV2Ver2BypassPurifierStatus.java | 159 ++++++++ ...yncV1AirPurifierDeviceDetailsResponse.java | 23 ++ .../handlers/VeSyncBaseDeviceHandler.java | 29 +- .../VeSyncDeviceAirHumidifierHandler.java | 210 ++++++++--- .../VeSyncDeviceAirPurifierHandler.java | 340 ++++++++++++------ .../VeSyncDeviceHumidifierMetadata.java | 97 +++++ .../VeSyncDevicePurifierMetadata.java | 71 ++++ .../resources/OH-INF/i18n/vesync.properties | 5 + .../resources/OH-INF/thing/thing-types.xml | 21 +- 23 files changed, 1308 insertions(+), 208 deletions(-) create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1Command.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetLevel.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetMode.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetStatus.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2Ver2BypassHumidifierStatus.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2Ver2BypassPurifierStatus.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceHumidifierMetadata.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDevicePurifierMetadata.java diff --git a/bundles/org.openhab.binding.vesync/README.md b/bundles/org.openhab.binding.vesync/README.md index f41dc8d548d08..61881f404f280 100644 --- a/bundles/org.openhab.binding.vesync/README.md +++ b/bundles/org.openhab.binding.vesync/README.md @@ -9,8 +9,8 @@ Air Humidifier models supported are Dual 200S, Classic 300S, 600S, OasisMist Sma ## 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) +Air Filtering models supported are Core200S, Core600S, 131S models and the Vital 100S, 200S. +Air Humidifier Classic 200S (Same as 300S without the nightlight from initial checks), OasisMist 1000 Smart Humidifier ## Supported Things @@ -66,43 +66,48 @@ 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 | | +| Channel | Type | Description | Model's Supported | Controllable Values | Unit | +|----------------------|----------------------|------------------------------------------------------------|------------------------------------------------|----------------------------|-------| +| **enabled** | Switch | Whether the hardware device is enabled (Switched on) | 131S, 600S, 400S, 300S, Vital 100S, Vital 200S | [ON, OFF] | | +| **childLock** | Switch | Whether the child lock (display lock is enabled) | 600S, 400S, 300S, Vital 100S, Vital 200S | [ON, OFF] | | +| **display** | Switch | Whether the display is enabled (display is shown) | 131S, 600S, 400S, 300S, Vital 100S, Vital 200S | [ON, OFF] | | +| **fanMode** | String | The operation mode of the fan | 131S, 600S, 400S, Vital 100S | [auto, manual, sleep] | | +| **fanMode** | String | The operation mode of the fan | 200S, 300S, | [manual, sleep] | | +| **fanMode** | String | The operation mode of the fan | Vital 200S | [auto, manual, sleep, pet] | | +| **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 | 131S, 300S | [1...3] | | +| **manualFanSpeed** | Number:Dimensionless | The speed of the fan when in manual mode | Vital 100S,Vital 200S | [1...5] | | +| **nightLightMode** | String | The night lights mode | 200S, 300S | [on, dim, off] | | +| filterLifePercentage | Number:Dimensionless | The remaining filter life as a percentage | 131S, 600S, 400S, 300S, Vital 100S, Vital 200S | | | +| airQuality | Number:Dimensionless | The air quality as represented by the Core200S / Core300S | 131S, 600S, 400S, 300S, Vital 100S, Vital 200S | | | +| airQualityPM25 | Number:Density | The air quality as represented by the Core400S | 600S, 400S, 300S, Vital 100S, Vital 200S | | µg/m³ | +| errorCode | Number:Dimensionless | The error code reported by the device | 600S, 400S, 300S, Vital 100S, Vital 200S | | | +| 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 | | | +| 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, OasisMist | [ON, OFF] | -| **display** | Switch | Whether the display is enabled (display is shown) | 200S, Dual200S, 300S, 600S, OasisMist | [ON, OFF] | -| waterLacking | Switch | Indicator whether the unit is lacking water | 200S, Dual200S, 300S, 600S, OasisMist | | -| humidityHigh | Switch | Indicator for high humidity | 200S, Dual200S, 300S, 600S, OasisMist | | -| waterTankLifted | Switch | Indicator for whether the water tank is removed | 200S, Dual200S, 300S, 600S, OasisMist | | -| **stopAtHumiditySetpoint** | Switch | Whether the unit is set to stop when the set point is reached | 200S, Dual200S, 300S, 600S, OasisMist | [ON, OFF] | -| humidity | Number:Dimensionless | Indicator for the currently measured humidity % level | 200S, Dual200S, 300S, 600S, OasisMist | | -| **mistLevel** | Number:Dimensionless | The current mist level set | 300S | [1...2] | -| **mistLevel** | Number:Dimensionless | The current mist level set | 200S, Dual200S, 600S, OasisMist | [1...3] | -| **humidifierMode** | String | The current mode of operation | 200S, Dual200S, 300S, 600S, OasisMist | [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, OasisMist | [30...80] | -| warmEnabled | Switch | Indicator for warm mist mode | 600S, OasisMist | | +| Channel | Type | Description | Model's Supported | Controllable Values | +|----------------------------|----------------------|---------------------------------------------------------------|------------------------------------------------------|---------------------| +| **enabled** | Switch | Whether the hardware device is enabled (Switched on) | 200S, Dual200S, 300S, 600S, OasisMist, OasisMist1000 | [ON, OFF] | +| **display** | Switch | Whether the display is enabled (display is shown) | 200S, Dual200S, 300S, 600S, OasisMist, OasisMist1000 | [ON, OFF] | +| waterLacking | Switch | Indicator whether the unit is lacking water | 200S, Dual200S, 300S, 600S, OasisMist, OasisMist1000 | | +| humidityHigh | Switch | Indicator for high humidity | 200S, Dual200S, 300S, 600S, OasisMist | | +| waterTankLifted | Switch | Indicator for whether the water tank is removed | 200S, Dual200S, 300S, 600S, OasisMist, OasisMist1000 | | +| **stopAtHumiditySetpoint** | Switch | Whether the unit is set to stop when the set point is reached | 200S, Dual200S, 300S, 600S, OasisMist, OasisMist1000 | [ON, OFF] | +| humidity | Number:Dimensionless | Indicator for the currently measured humidity % level | 200S, Dual200S, 300S, 600S, OasisMist, OasisMist1000 | | +| **mistLevel** | Number:Dimensionless | The current mist level set | 300S | [1...2] | +| **mistLevel** | Number:Dimensionless | The current mist level set | 200S, Dual200S, 600S, OasisMist, OasisMist1000 | [1...3] | +| **humidifierMode** | String | The current mode of operation | 200S, Dual200S, 300S, 600S, OasisMist, OasisMist1000 | [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, OasisMist, OasisMist1000 | [30...80] | +| warmEnabled | Switch | Indicator for warm mist mode | 600S, OasisMist | | +| **warmLevel** | Number:Dimensionless | The current warm mist level set | 600S, OasisMist | [0..3] | +| errorCode | Number:Dimensionless | The error code reported by the device | OasisMist1000 | | +| timerExpiry | DateTime | The expected expiry time of the current timer | OasisMist1000 | | +| schedulesCount | Number:Dimensionless | The number schedules configured | OasisMist1000 | | ## Full Example @@ -129,7 +134,7 @@ Switch LoungeAPControlsLock "Lounge Air Purifier Controls 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:Density LoungeAPAirQuality "Lounge Air Purifier Air Quality [%.0f% %unit%]" { unit="µg/m³",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" } @@ -137,7 +142,7 @@ DateTime LoungeAPTimerExpiry "Lounge Air Purifier Timer Ex Number LoungeAPSchedulesCount "Lounge Air Purifier Schedules Count" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:schedulesCount" } ``` -#### Air Purifier Core 200S/300S Model +#### Air Purifier Core 200S / 300S Model ```java Switch LoungeAPPower "Lounge Air Purifier Power" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:enabled" } @@ -147,7 +152,7 @@ Switch LoungeAPControlsLock "Lounge Air Purifier Controls 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:Density LoungeAPAirQuality "Lounge Air Purifier Air Quality [%.0f%]" { unit="µg/m³",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" } @@ -155,6 +160,30 @@ DateTime LoungeAPTimerExpiry "Lounge Air Purifier Timer Ex Number LoungeAPSchedulesCount "Lounge Air Purifier Schedules Count" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:schedulesCount" } ``` +#### Air Purifier 131s Models + +```java +Switch LoungeAPPower "Lounge Air Purifier Power" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:enabled" } +Switch LoungeAPDisplay "Lounge Air Purifier Display" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:display" } +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:Dimensionless LoungeAPAirQuality "Lounge Air Purifier Air Quality" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:airQuality" } +``` + +#### Air Purifier Vital 100s / 200s Models + +```java +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" } +``` + #### Air Humidifier Classic 200S / Dual 200S Model ```java @@ -199,6 +228,7 @@ Number:Dimensionless LoungeAHHumidity "Lounge Air Humidifier Measured H 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" } +Number:Dimensionless LoungeAHWarmMistLevel "Lounge Air Humidifier Warm Mist Level" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:warmLevel" } ``` #### Air Humidifier Oasis Mist Smart Model @@ -214,8 +244,29 @@ Number:Dimensionless LoungeAHHumidity "Lounge Air Humidifier Measured H 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" } +Number:Dimensionless LoungeAHWarmMistLevel "Lounge Air Humidifier Warm Mist Level" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:warmLevel" } ``` +#### Air Humidifier Oasis Mist 1000 Smart Model + +```java +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" } +Number:Dimensionless LoungeAHWarmMistLevel "Lounge Air Humidifier Warm Mist Level" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:warmLevel" } +DateTime LoungeAHTimerExpiry "Lounge Air Humidifier Timer Expiry [%1$tA %1$tI:%1$tM %1$Tp]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:timerExpiry" } +Number LoungeAHSchedulesCount "Lounge Air Humidifier Schedules Count" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:schedulesCount" } +Number LoungeAHErrorCode "Lounge Air Humidifier Error Code" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:errorCode" } +``` + + ### Configuration (*.sitemap) #### Air Purifier Core 400S / 600S Model @@ -234,7 +285,7 @@ Frame { } ``` -#### Air Purifier Core 200S/300S Model +#### Air Purifier Core 200S / 300S Model ```perl Frame { @@ -251,6 +302,47 @@ Frame { } ``` +#### Air Purifier 131s Models + +```perl +Frame { + Switch item=LoungeAPPower label="Power" + Text item=LoungeAPFilterRemainingUse label="Filter Remaining" + Switch item=LoungeAPDisplay label="Display" + Text item=LoungeAPAirQuality label="Air Quality [%.0f]" + Switch item=LoungeAPMode label="Mode" mappings=[auto="Auto",manual="Manual Fan Control", sleep="Sleeping"] icon="settings" + Switch item=LoungeAPManualFanSpeed label="Manual Fan Speed [%.0f]" mappings=[1="1", 2="2", 3="3"] icon="settings" +} +``` + +#### Air Purifier Vital 100S Models + +```perl +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" + Switch item=LoungeAPMode label="Mode" mappings=[auto="Auto", manual="Manual Fan Control", sleep="Sleeping"] icon="settings" + Switch item=LoungeAPManualFanSpeed label="Manual Fan Speed [%.0f]" mappings=[1="1", 2="2", 3="3", 4="4", 5="5"] icon="settings" +} +``` + +#### Air Purifier Vital 200S Models + +```perl +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" + Switch item=LoungeAPMode label="Mode" mappings=[auto="Auto", manual="Manual Fan Control", sleep="Sleeping", pet="Pet"] icon="settings" + Switch item=LoungeAPManualFanSpeed label="Manual Fan Speed [%.0f]" mappings=[1="1", 2="2", 3="3", 4="4", 5="5"] icon="settings" +} +``` + #### Air Humidifier Classic 200S / Dual 200S Model ```perl @@ -282,7 +374,7 @@ Frame { Text icon="none" item=LoungeAHHumidity Switch item=LoungeAHTargetStop Slider item=LoungeAHTarget minValue=30 maxValue=80 - Slider item=LoungeAHMistLevel minValue=1 maxValue=3 + Slider item=LoungeAHMistLevel minValue=0 maxValue=3 } ``` @@ -292,7 +384,7 @@ Frame { Frame { Switch item=LoungeAHPower Switch item=LoungeAHDisplay - Switch item=LoungeAHMode label="Mode" mappings=[auto="Auto", sleep="Sleeping"] icon="settings" + Switch item=LoungeAHMode label="Mode" mappings=[auto="Auto", manual="Manual Control", sleep="Sleeping"] icon="settings" Text icon="none" item=LoungeAHWaterLacking Text icon="none" item=LoungeAHHighHumidity Text icon="none" item=LoungeAHWaterTankRemoved @@ -300,6 +392,7 @@ Frame { Switch item=LoungeAHTargetStop Slider item=LoungeAHTarget minValue=30 maxValue=80 Slider item=LoungeAHMistLevel minValue=1 maxValue=3 + Slider item=LoungeAHWarmMistLevel minValue=0 maxValue=3 } ``` @@ -317,6 +410,27 @@ Frame { Switch item=LoungeAHTargetStop Slider item=LoungeAHTarget minValue=30 maxValue=80 Slider item=LoungeAHMistLevel minValue=1 maxValue=3 + Slider item=LoungeAHWarmMistLevel minValue=1 maxValue=3 +} +``` + +#### Air Humidifier Oasis Mist 1000 Smart Model + +```perl +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 + Slider item=LoungeAHWarmMistLevel minValue=1 maxValue=3 + Text item=LoungeAHTimerExpiry label="Timer Shutdown @" icon="clock" + Text item=LoungeAHErrorCode label="Error Code [%.0f]" } ``` 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 index 33f5972f77239..95e7890df66d8 100644 --- 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 @@ -30,7 +30,7 @@ public class VeSyncConstants { public static final Gson GSON = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting() - .disableHtmlEscaping().serializeNulls().create(); + .disableHtmlEscaping().create(); private static final String BINDING_ID = "vesync"; @@ -65,6 +65,8 @@ public class VeSyncConstants { 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"; + public static final String DEVICE_CHANNEL_AF_LIGHT_DETECTION = "lightDetection"; + public static final String DEVICE_CHANNEL_AF_LIGHT_DETECTED = "lightDetected"; // Humidity related channels public static final String DEVICE_CHANNEL_WATER_LACKS = "waterLacking"; 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 index bef2421582d1e..77d4309d939cc 100644 --- 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 @@ -171,12 +171,13 @@ public String reqV1Authorized(final String url, final VeSyncAuthenticatedRequest private String directReqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData) throws AuthenticationException { try { - Request request = httpClient.POST(url); + Request request = httpClient.newRequest(url).method(requestData.httpMethod); // No headers for login request.content(new StringContentProvider(VeSyncConstants.GSON.toJson(requestData))); - logger.debug("POST @ {} with content\r\n{}", url, VeSyncConstants.GSON.toJson(requestData)); + logger.debug("{} @ {} with content\r\n{}", requestData.httpMethod, url, + VeSyncConstants.GSON.toJson(requestData)); request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8"); 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 index ec0e0c92ef858..3de7a18708bbc 100644 --- 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 @@ -106,6 +106,9 @@ protected void stopBackgroundDiscovery() { @Override protected void startScan() { + if (bridgeHandler == null) { + return; + } // 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())) { 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 index 0fdc8744ba443..529c8afa95aec 100644 --- 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 @@ -23,6 +23,8 @@ public interface VeSyncProtocolConstants { String MODE_AUTO = "auto"; String MODE_MANUAL = "manual"; String MODE_SLEEP = "sleep"; + String MODE_PET = "pet"; + String MODE_AUTO_HUMIDITY = "humidity"; String MODE_ON = "on"; String MODE_DIM = "dim"; @@ -42,6 +44,7 @@ public interface VeSyncProtocolConstants { String DEVICE_GET_HUMIDIFIER_STATUS = "getHumidifierStatus"; String DEVICE_LEVEL_TYPE_MIST = "mist"; + String DEVICE_LEVEL_TYPE_WARM_MIST = "warm"; // Air Purifier Commands String DEVICE_SET_PURIFIER_MODE = "setPurifierMode"; @@ -49,12 +52,16 @@ public interface VeSyncProtocolConstants { String DEVICE_SET_NIGHT_LIGHT = "setNightLight"; String DEVICE_GET_PURIFIER_STATUS = "getPurifierStatus"; String DEVICE_LEVEL_TYPE_WIND = "wind"; + String DEVICE_SET_LIGHT_DETECTION = "setLightDetection"; /** * Base URL for AUTHENTICATION REQUESTS */ String PROTOCOL = "https"; - String HOST_ENDPOINT = PROTOCOL + "://smartapi.vesync.com/cloud"; + String SERVER_ADDRESS = "smartapi.vesync.com"; + String SERVER_ENDPOINT = PROTOCOL + "://" + SERVER_ADDRESS; + + String HOST_ENDPOINT = SERVER_ENDPOINT + "/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 index cee7608101846..2d7e5195a2d95 100644 --- 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 @@ -12,6 +12,8 @@ */ package org.openhab.binding.vesync.internal.dto.requests; +import javax.ws.rs.HttpMethod; + import com.google.gson.annotations.SerializedName; /** @@ -21,6 +23,8 @@ */ public class VeSyncRequest { + public transient String httpMethod; + @SerializedName("timeZone") public String timeZone = "America/New_York"; @@ -44,5 +48,6 @@ public class VeSyncRequest { public VeSyncRequest() { traceId = String.valueOf(System.currentTimeMillis()); + httpMethod = HttpMethod.POST; } } 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 index aea75c0e82d3a..d4f56fe4585be 100644 --- 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 @@ -55,6 +55,75 @@ public class VesyncManagedDeviceBase { public static class EmptyPayload { } + public static class SetLightDetectionPayload extends EmptyPayload { + + public SetLightDetectionPayload(final boolean enabled) { + lightDetectionSwitch = enabled ? 1 : 0; + } + + @SerializedName("lightDetectionSwitch") + public int lightDetectionSwitch = -1; + } + + public static class SetPowerPayload extends EmptyPayload { + + public SetPowerPayload(final boolean enabled, final int switchIdx) { + this.powerSwitch = enabled ? 1 : 0; + this.switchIdx = switchIdx; + } + + @SerializedName("switchIdx") + public int switchIdx = -1; + + @SerializedName("powerSwitch") + public int powerSwitch = -1; + } + + public static class SetChildLockPayload extends EmptyPayload { + + public SetChildLockPayload(final boolean enabled) { + this.childLockSwitch = enabled ? 1 : 0; + } + + @SerializedName("childLockSwitch") + public int childLockSwitch = -1; + } + + public static class SetScreenSwitchPayload extends EmptyPayload { + + public SetScreenSwitchPayload(final boolean enabled) { + this.screenSwitch = enabled ? 1 : 0; + } + + @SerializedName("screenSwitch") + public int screenSwitch = -1; + } + + public static class SetManualSpeedLevelPayload extends EmptyPayload { + + public SetManualSpeedLevelPayload(final int manualSpeedLevel) { + this.manualSpeedLevel = manualSpeedLevel; + } + + @SerializedName("levelIdx") + public int levelIdx = 0; + + @SerializedName("levelType") + public String levelType = "wind"; + + @SerializedName("manualSpeedLevel") + public int manualSpeedLevel = -1; + } + + public static class SetWorkModePayload extends EmptyPayload { + public SetWorkModePayload(final String workMode) { + this.workMode = workMode; + } + + @SerializedName("workMode") + public String workMode = ""; + } + public static class SetSwitchPayload extends EmptyPayload { public SetSwitchPayload(final boolean enabled, final int id) { diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1Command.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1Command.java new file mode 100644 index 0000000000000..a1452f09695df --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1Command.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import javax.ws.rs.HttpMethod; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncRequestV1Command} is the Java class as a DTO to define the base implementation of a V1 command for + * the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestV1Command extends VeSyncAuthenticatedRequest { + + @SerializedName("uuid") + public String uuid = null; + + public VeSyncRequestV1Command(final String deviceUuid) { + // Exclude fields that shouldn't be there by setting to null + super.phoneOS = null; + super.phoneBrand = null; + super.method = null; + super.appVersion = null; + super.httpMethod = HttpMethod.PUT; + // Set the required payload parameters + uuid = deviceUuid; + } + + public String getUuid() { + return uuid; + } +} 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 index e5c04095ec36f..8d1b8201e4d0a 100644 --- 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 @@ -18,7 +18,8 @@ import com.google.gson.annotations.SerializedName; /** - * The {@link VeSyncRequestV1ManagedDeviceDetails} is the Java class as a DTO to hold login credentials for the Vesync + * The {@link VeSyncRequestV1ManagedDeviceDetails} is the Java class as a DTO to request the managed device details for + * the Vesync * API. * * @author David Goodyear - Initial contribution diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetLevel.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetLevel.java new file mode 100644 index 0000000000000..ea50568d0cf76 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetLevel.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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 VeSyncRequestV1SetLevel} is the Java class as a DTO define a V1 Set Level command for the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestV1SetLevel extends VeSyncRequestV1Command { + + @SerializedName("level") + public Integer level = null; + + public VeSyncRequestV1SetLevel(final String deviceUuid, final int level) { + super(deviceUuid); + this.level = level; + } + + public Integer getLevel() { + return level; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetMode.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetMode.java new file mode 100644 index 0000000000000..b96f6763d7978 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetMode.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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 VeSyncRequestV1SetMode} is the Java class as a DTO define a V1 Set Mode command for the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestV1SetMode extends VeSyncRequestV1Command { + + @SerializedName("mode") + public String mode = null; + + public VeSyncRequestV1SetMode(final String deviceUuid, final String mode) { + super(deviceUuid); + this.mode = mode; + } + + public String getMode() { + return mode; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetStatus.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetStatus.java new file mode 100644 index 0000000000000..80a1d292be7cd --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetStatus.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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 VeSyncRequestV1SetStatus} is the Java class as a DTO define a V1 Set Status command for the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestV1SetStatus extends VeSyncRequestV1Command { + + @SerializedName("status") + public String status = null; + + public VeSyncRequestV1SetStatus(final String deviceUuid, final String status) { + super(deviceUuid); + this.status = status; + } + + public String getStatus() { + return status; + } +} 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 index 193fd6acae8b0..fb3ab6ad455ce 100644 --- 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 @@ -16,8 +16,7 @@ /** * The {@link VeSyncV2BypassPurifierStatus} is a Java class used as a DTO to hold the Vesync's API's common response - * data, - * in regards to an Air Purifier device. + * data, in regards to an Air Purifier device. * * @author David Goodyear - Initial contribution */ diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2Ver2BypassHumidifierStatus.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2Ver2BypassHumidifierStatus.java new file mode 100644 index 0000000000000..d0175a5cb01e4 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2Ver2BypassHumidifierStatus.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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 VeSyncV2Ver2BypassHumidifierStatus} is a Java class used as a DTO to hold the Vesync's API's common + * response data, in regard's to a Air Humidifier based device, using the latest encoding protocol scheme. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncV2Ver2BypassHumidifierStatus extends VeSyncResponse { + + @SerializedName("result") + public VeSyncV2Ver2BypassHumidifierStatus.HumidifierStatus result; + + public class HumidifierStatus extends VeSyncResponse { + + @SerializedName("result") + public VeSyncV2Ver2BypassHumidifierStatus.HumidifierStatus.AirHumidifierStatus result; + + public class AirHumidifierStatus { + + @SerializedName("powerSwitch") + public int powerSwitch; + + public boolean getPowerSwitch() { + return powerSwitch == 1; + } + + @SerializedName("virtualLevel") + public int virtualLevel; + + @SerializedName("mistLevel") + public int mistLevel; + + @SerializedName("workMode") + public String workMode; + + @SerializedName("waterLacksState") + public int waterLacksState; + + public boolean getWaterLacksState() { + return waterLacksState == 1; + } + + @SerializedName("targetHumidity") + public int targetHumidity; + + @SerializedName("autoStopState") + public int autoStopState; + + public boolean getAutoStopState() { + return autoStopState == 1; + } + + @SerializedName("screenState") + public int screenState; + + public boolean getScreenState() { + return screenState == 1; + } + + @SerializedName("screenSwitch") + public int screenSwitch; + + public boolean getScreenSwitch() { + return screenSwitch == 1; + } + + @SerializedName("humidity") + public int humidity; + + @SerializedName("waterTankLifted") + public int waterTankLifted; + + public boolean getWaterTankLifted() { + return waterTankLifted == 1; + } + + @SerializedName("autoStopSwitch") + public int autoStopSwitch; + + public boolean getAutoStopSwitch() { + return autoStopSwitch == 1; + } + + @SerializedName("scheduleCount") + public int scheduleCount; + + @SerializedName("timerRemain") + public int timerRemain; + + @SerializedName("errorCode") + public int errorCode; + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2Ver2BypassPurifierStatus.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2Ver2BypassPurifierStatus.java new file mode 100644 index 0000000000000..4fd8b3d0b4543 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2Ver2BypassPurifierStatus.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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 VeSyncV2Ver2BypassPurifierStatus} is a Java class used as a DTO to hold the Vesync's API's common + * response data, in regards to an Air Purifier based device, using the latest encoding protocol scheme. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncV2Ver2BypassPurifierStatus extends VeSyncResponse { + + @SerializedName("result") + public PurifierStatus result; + + public class PurifierStatus extends VeSyncResponse { + + @SerializedName("result") + public AirPurifierStatus result; + + public class AirPurifierStatus { + @SerializedName("AQLevel") + public int airQuality; + + @SerializedName("powerSwitch") + public int powerSwitch; + + public boolean getPowerSwitch() { + return powerSwitch == 1; + } + + @SerializedName("workMode") + public String workMode; + + @SerializedName("fanSpeedLevel") + public int fanSpeedLevel; + + @SerializedName("manualSpeedLevel") + public int manualSpeedLevel; + + @SerializedName("filterLifePercent") + public int filterLifePercent; + + @SerializedName("childLockSwitch") + public int childLockSwitch; + + public boolean getChildLockSwitch() { + return childLockSwitch == 1; + } + + @SerializedName("screenState") + public int screenState; + + public boolean getScreenState() { + return screenState == 1; + } + + @SerializedName("lightDetectionSwitch") + public int lightDetectionSwitch; + + public boolean getLightDetectionSwitch() { + return lightDetectionSwitch == 1; + } + + @SerializedName("environmentLightState") + public int environmentLightState; + + public boolean getEnvironmentLightState() { + return environmentLightState == 1; + } + + @SerializedName("screenSwitch") + public int screenSwitch; + + public boolean getScreenSwitch() { + return screenSwitch == 1; + } + + @SerializedName("PM25") + public int PM25; + + @SerializedName("timerRemain") + public int timerRemain; + + @SerializedName("scheduleCount") + public int scheduleCount; + + @SerializedName("efficientModeTimeRemain") + public int efficientModeTimeRemain; + + @SerializedName("errorCode") + public int errorCode; + + @SerializedName("autoPreference") + public VeSyncV2Ver2BypassPurifierStatus.PurifierStatus.AirPurifierStatus.AirPurifierConfigAutoPref autoPreference; + + public class AirPurifierConfigAutoPref { + @SerializedName("autoPreferenceType") + public String autoType; + + @SerializedName("roomSize") + public int roomSize; + } + + @SerializedName("sleepPreference") + public VeSyncV2Ver2BypassPurifierStatus.PurifierStatus.AirPurifierStatus.AirPurifierSleepPref sleepPreference; + + public class AirPurifierSleepPref { + @SerializedName("sleepPreferenceType") + public String sleepPreferenceType; + + @SerializedName("cleaningBeforeBedSwitch") + public int cleaningBeforeBedSwitch; + + @SerializedName("cleaningBeforeBedSpeedLevel") + public int cleaningBeforeBedSpeedLevel; + + @SerializedName("cleaningBeforeBedMinutes") + public int cleaningBeforeBedMinutes; + + @SerializedName("whiteNoiseSleepAidSwitch") + public int whiteNoiseSleepAidSwitch; + + @SerializedName("whiteNoiseSleepAidSpeedLevel") + public int whiteNoiseSleepAidSpeedLevel; + + @SerializedName("whiteNoiseSleepAidMinutes") + public int whiteNoiseSleepAidMinutes; + + @SerializedName("duringSleepSpeedLevel") + public int duringSleepSpeedLevel; + + @SerializedName("duringSleepMinutes") + public int duringSleepMinutes; + + @SerializedName("afterWakeUpPowerSwitch") + public int afterWakeUpPowerSwitch; + + @SerializedName("afterWakeUpWorkMode") + public String afterWakeUpWorkMode; + + @SerializedName("afterWakeUpFanSpeedLevel") + public String afterWakeUpFanSpeedLevel; + } + } + } +} 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 index 356f31adc663d..20e248499870a 100644 --- 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 @@ -53,6 +53,29 @@ public String getMode() { return mode; } + @SerializedName("activeTime") + public int activeTime; + + public int getActiveTime() { + return activeTime; + } + + @SerializedName("filterLife") + public FilterLife filter; + + public int getFilterPercent() { + return filter.getPercent(); + } + + public class FilterLife { + @SerializedName("percent") + public int percent; + + public int getPercent() { + return percent; + } + } + @SerializedName("deviceName") public String deviceName; 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 index 260a7df858896..13c2eeb976ee0 100644 --- 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 @@ -32,6 +32,7 @@ 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.VeSyncProtocolConstants; 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; @@ -47,6 +48,7 @@ import org.openhab.core.thing.binding.BridgeHandler; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.types.State; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -415,7 +417,20 @@ protected final String sendV2BypassControlCommand(final String method, return result; } - public final String sendV1Command(final String method, final String url, final VeSyncAuthenticatedRequest request) { + protected final String sendV1ControlCommand(final String urlPath, final VeSyncAuthenticatedRequest request) { + return sendV1ControlCommand(urlPath, request, true); + } + + protected final String sendV1ControlCommand(final String urlPath, final VeSyncAuthenticatedRequest request, + final boolean readbackDevice) { + final String result = sendV1Command(urlPath, request); + if (!result.equals(EMPTY_STRING) && readbackDevice) { + performReadbackPoll(); + } + return result; + } + + public final String sendV1Command(final String urlPath, final VeSyncAuthenticatedRequest request) { if (ThingStatus.OFFLINE.equals(this.thing.getStatus())) { logger.debug("Command blocked as device is offline"); return EMPTY_STRING; @@ -427,6 +442,7 @@ public final String sendV1Command(final String method, final String url, final V } VeSyncClient client = getVeSyncClient(); if (client != null) { + final String url = VeSyncProtocolConstants.SERVER_ENDPOINT + "/" + urlPath; return client.reqV2Authorized(url, deviceLookupKey, request); } else { throw new DeviceUnknownException("Missing client"); @@ -552,4 +568,15 @@ public static VeSyncDeviceMetadata getDeviceFamilyMetadata(final @Nullable Strin public VeSyncDeviceMetadata getDeviceFamilyMetadata(final @Nullable String deviceType) { return getDeviceFamilyMetadata(deviceType, getDeviceFamilyProtocolPrefix(), getSupportedDeviceMetadata()); } + + @Override + protected void updateState(final String channelID, final State state) { + // In case of any unexpected decoding issues log them, so that the necessary adjustments can + // be done. (Not expected but just in case). + try { + super.updateState(channelID, state); + } catch (final Exception e) { + logger.warn("Please report issue - could not update channel {} with error {}", channelID, e.toString()); + } + } } 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 index d3b2464c07923..4ff60d89208c2 100644 --- 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 @@ -15,17 +15,26 @@ 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.Collections; +import java.util.HashMap; import java.util.List; +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.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.VeSyncResponse; import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassHumidifierStatus; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2Ver2BypassHumidifierStatus; 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; @@ -47,6 +56,7 @@ * @author David Goodyear - Initial contribution */ @NonNullByDefault +@SuppressWarnings("serial") public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler { public static final String DEV_TYPE_FAMILY_AIR_HUMIDIFIER = "LUH"; @@ -59,33 +69,58 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler { public static final String DEV_FAMILY_600S = "600S"; public static final String DEV_FAMILY_OASIS_MIST = "Oasis Mist"; - public static final VeSyncDeviceMetadata CLASSIC200S = new VeSyncDeviceMetadata(DEV_FAMILY_CLASSIC_200S, - Collections.emptyList(), List.of("Classic200S")); + public static final String DEV_FAMILY_OASIS_MIST_1000 = "Oasis Mist 1000"; - public static final VeSyncDeviceMetadata CLASSIC300S = new VeSyncDeviceMetadata(DEV_FAMILY_CLASSIC_300S, - Arrays.asList("A601S"), List.of("Classic300S")); + private static final List AUTO_MAN_SLEEP_MODES = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP); - public static final VeSyncDeviceMetadata DUAL200S = new VeSyncDeviceMetadata(DEV_FAMILY_DUAL_200S, - Arrays.asList("D301S"), List.of("Dual200S")); + private static final List AUTO_MAN_MODES = Arrays.asList(MODE_AUTO, MODE_MANUAL); - public static final VeSyncDeviceMetadata LV600S = new VeSyncDeviceMetadata(DEV_FAMILY_600S, Arrays.asList("A602S"), + private static final List CLASSIC_300S_NIGHT_LIGHT_MODES = Arrays.asList(MODE_ON, MODE_DIM, MODE_OFF); + + public static final VeSyncDeviceHumidifierMetadata CLASSIC200S = new VeSyncDeviceHumidifierMetadata(1, + DEV_FAMILY_CLASSIC_200S, Collections.emptyList(), List.of("Classic200S"), AUTO_MAN_MODES, 1, 3, -1, -1, + false, Collections.emptyList()); + + public static final VeSyncDeviceHumidifierMetadata CLASSIC300S = new VeSyncDeviceHumidifierMetadata(1, + DEV_FAMILY_CLASSIC_300S, Arrays.asList("A601S"), List.of("Classic300S"), AUTO_MAN_SLEEP_MODES, 1, 3, -1, -1, + false, CLASSIC_300S_NIGHT_LIGHT_MODES); + + public static final VeSyncDeviceHumidifierMetadata DUAL200S = new VeSyncDeviceHumidifierMetadata(1, + DEV_FAMILY_DUAL_200S, Arrays.asList("D301S"), List.of("Dual200S"), AUTO_MAN_MODES, 1, 2, -1, -1, false, Collections.emptyList()); - public static final VeSyncDeviceMetadata OASIS_MIST = new VeSyncDeviceMetadata(DEV_FAMILY_OASIS_MIST, - Arrays.asList("O451S"), Collections.emptyList()); + public static final VeSyncDeviceHumidifierMetadata LV600S = new VeSyncDeviceHumidifierMetadata(1, DEV_FAMILY_600S, + Arrays.asList("A602S"), Collections.emptyList(), AUTO_MAN_SLEEP_MODES, 1, 3, 0, 3, true, + CLASSIC_300S_NIGHT_LIGHT_MODES); + + public static final VeSyncDeviceHumidifierMetadata OASIS_MIST = new VeSyncDeviceHumidifierMetadata(1, + DEV_FAMILY_OASIS_MIST, Arrays.asList("O451S"), Collections.emptyList(), AUTO_MAN_SLEEP_MODES, 1, 3, 0, 3, + true, Collections.emptyList()); + + public static final VeSyncDeviceHumidifierMetadata OASIS_MIST_1000 = new VeSyncDeviceHumidifierMetadata(2, + DEV_FAMILY_OASIS_MIST_1000, Arrays.asList("M101S"), Collections.emptyList(), AUTO_MAN_SLEEP_MODES, 1, 3, 0, + 3, false, Collections.emptyList()); public static final List SUPPORTED_MODEL_FAMILIES = Arrays.asList(LV600S, CLASSIC300S, CLASSIC200S, DUAL200S, OASIS_MIST); - 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); - 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 static final Map DEV_FAMILY_HUMIDIFER_MAP = new HashMap() { + { + put(CLASSIC200S.deviceFamilyName, CLASSIC200S); + put(CLASSIC300S.deviceFamilyName, CLASSIC300S); + put(DUAL200S.deviceFamilyName, DUAL200S); + put(LV600S.deviceFamilyName, LV600S); + put(OASIS_MIST.deviceFamilyName, OASIS_MIST); + put(OASIS_MIST_1000.deviceFamilyName, OASIS_MIST_1000); + } + }; + public VeSyncDeviceAirHumidifierHandler(Thing thing) { super(thing); } @@ -97,15 +132,22 @@ protected String[] getChannelsToRemove() { if (deviceFamily != null) { switch (deviceFamily) { case DEV_FAMILY_CLASSIC_300S: - toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL }; + toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL, + DEVICE_CHANNEL_AF_SCHEDULES_COUNT, DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME }; break; case DEV_FAMILY_DUAL_200S: case DEV_FAMILY_CLASSIC_200S: + toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL, + DEVICE_CHANNEL_AF_NIGHT_LIGHT, DEVICE_CHANNEL_AF_SCHEDULES_COUNT, + DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME }; + break; + case DEV_FAMILY_OASIS_MIST_1000: toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL, DEVICE_CHANNEL_AF_NIGHT_LIGHT }; break; case DEV_FAMILY_OASIS_MIST: - toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT }; + toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT, DEVICE_CHANNEL_AF_SCHEDULES_COUNT, + DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME }; break; } } @@ -152,6 +194,11 @@ public void handleCommand(final ChannelUID channelUID, final Command command) { if (deviceFamily == null) { return; } + final VeSyncDeviceHumidifierMetadata devContraints = DEV_FAMILY_HUMIDIFER_MAP.get(deviceFamily); + if (devContraints == null) { + logger.warn("Could not find device family for {} during handleCommand", deviceFamily); + return; + } scheduler.submit(() -> { @@ -187,31 +234,27 @@ public void handleCommand(final ChannelUID channelUID, final Command command) { } sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE, - new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_AUTO), false); + new VeSyncRequestManagedDeviceBypassV2.SetMode( + devContraints.getProtocolMode(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 (!devContraints.isTargetMistLevelSupported(targetMistLevel)) { + logger.warn("Mist level command for \"{}\" is not valid ({}) API possible options {} -> {}", + command, devContraints.deviceFamilyName, devContraints.targetMinMistLevel, + devContraints.targetMaxMistLevel); + targetMistLevel = targetMistLevel < devContraints.targetMinMistLevel + ? devContraints.targetMinMistLevel + : devContraints.targetMaxMistLevel; + } + // 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_FAMILY_DUAL_200S.equals(deviceFamily)) { - 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; - } + if (!DEV_FAMILY_DUAL_200S.equals(deviceFamily)) { // Re-map to what appears to be bitwise encoding of the states switch (targetMistLevel) { case 1: @@ -234,33 +277,45 @@ public void handleCommand(final ChannelUID channelUID, final Command command) { targetMistLevel)); break; case DEVICE_CHANNEL_WARM_LEVEL: - logger.warn("Warm level API is unknown in order to send the command"); + int targetWarmMistLevel = ((QuantityType) command).intValue(); + if (!devContraints.isTargetWramMistLevelSupported(targetWarmMistLevel)) { + logger.warn( + "Warm mist level command for \"{}\" is not valid ({}) API possible options {} -> {}", + command, devContraints.deviceFamilyName, devContraints.targetMinWarmMistLevel, + devContraints.targetMaxWarmMistLevel); + targetWarmMistLevel = targetWarmMistLevel < devContraints.targetMinWarmMistLevel + ? devContraints.targetMinWarmMistLevel + : devContraints.targetMaxWarmMistLevel; + } + + sendV2BypassControlCommand(DEVICE_SET_LEVEL, + new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_WARM_MIST, + targetWarmMistLevel)); 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)) { + if (!devContraints.fanModes.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)); + "Humidifier mode command for \"{}\" is not valid in the ({}}) API possible options {}", + command, devContraints.deviceFamilyName, String.join(",", devContraints.fanModes)); return; } sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE, - new VeSyncRequestManagedDeviceBypassV2.SetMode(targetMode)); + new VeSyncRequestManagedDeviceBypassV2.SetMode( + devContraints.getProtocolMode(targetMode))); break; case DEVICE_CHANNEL_AF_NIGHT_LIGHT: - if (!DEV_FAMILY_CLASSIC_300S.equals(deviceFamily) && !DEV_FAMILY_600S.equals(deviceFamily)) { - logger.warn("Humidifier night light is not valid for your device ({}})", deviceFamily); - return; - } - if (!CLASSIC_300S_NIGHT_LIGHT_MODES.contains(targetMode)) { + if (!devContraints.nightLightModes.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)); + "Humidifier night light command for \"{}\" is not valid in the ({}}) API possible options {}", + command, devContraints.deviceFamilyName, + String.join(",", devContraints.nightLightModes)); return; } + int targetValue; switch (targetMode) { case MODE_OFF: @@ -289,7 +344,16 @@ public void handleCommand(final ChannelUID channelUID, final Command command) { @Override protected void pollForDeviceData(final ExpiringCache cachedResponse) { String response; - VeSyncV2BypassHumidifierStatus humidifierStatus; + VeSyncResponse humidifierStatus; + + final String deviceFamily = getThing().getProperties().get(DEVICE_PROP_DEVICE_FAMILY); + + final VeSyncDeviceHumidifierMetadata devContraints = DEV_FAMILY_HUMIDIFER_MAP.get(deviceFamily); + if (devContraints == null) { + logger.warn("Could not find device family for {} during pollForDeviceData", deviceFamily); + return; + } + synchronized (pollLock) { response = cachedResponse.getValue(); boolean cachedDataUsed = response != null; @@ -305,7 +369,11 @@ protected void pollForDeviceData(final ExpiringCache cachedResponse) { return; } - humidifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassHumidifierStatus.class); + if (devContraints.protocolV2Version == 2) { + humidifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2Ver2BypassHumidifierStatus.class); + } else { + humidifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassHumidifierStatus.class); + } if (humidifierStatus == null) { return; @@ -325,13 +393,20 @@ protected void pollForDeviceData(final ExpiringCache cachedResponse) { updateStatus(ThingStatus.ONLINE); } + if (devContraints.protocolV2Version != 2) { + parseV2Ver1Poll((VeSyncV2BypassHumidifierStatus) humidifierStatus, deviceFamily); + } else { + parseV2Ver2Poll((VeSyncV2Ver2BypassHumidifierStatus) humidifierStatus); + } + } + + private void parseV2Ver1Poll(final VeSyncV2BypassHumidifierStatus humidifierStatus, + final @Nullable String deviceFamily) { if (!"0".equals(humidifierStatus.result.getCode())) { - logger.warn("Check correct Thing type has been set - API gave a unexpected response for an Air Humidifier"); + logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Humidifier"); return; } - final String deviceFamily = getThing().getProperties().get(DEVICE_PROP_DEVICE_FAMILY); - 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)); @@ -342,6 +417,10 @@ protected void pollForDeviceData(final ExpiringCache cachedResponse) { updateState(DEVICE_CHANNEL_HUMIDITY, new QuantityType<>(humidifierStatus.result.result.humidity, Units.PERCENT)); updateState(DEVICE_CHANNEL_MIST_LEVEL, new DecimalType(humidifierStatus.result.result.mistLevel)); + // Map back HUMIDITY -> AUTO if necessary for devices where auto is remapped + if (MODE_AUTO_HUMIDITY.equals(humidifierStatus.result.result.mode)) { + humidifierStatus.result.result.mode = MODE_AUTO; + } updateState(DEVICE_CHANNEL_HUMIDIFIER_MODE, new StringType(humidifierStatus.result.result.mode)); // Only the 300S supports nightlight currently of tested devices. @@ -354,12 +433,43 @@ protected void pollForDeviceData(final ExpiringCache cachedResponse) { } else { updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_DIM)); } - } else if (DEV_FAMILY_600S.equals(deviceFamily) || DEV_FAMILY_OASIS_MIST.equals(deviceFamily)) { + } + if (DEV_FAMILY_600S.equals(deviceFamily) || DEV_FAMILY_OASIS_MIST.equals(deviceFamily)) { 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)); } + + private void parseV2Ver2Poll(final VeSyncV2Ver2BypassHumidifierStatus humidifierStatus) { + if (!"0".equals(humidifierStatus.result.getCode())) { + logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Humidifier"); + return; + } + + updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(humidifierStatus.result.result.getPowerSwitch())); + updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(humidifierStatus.result.result.getScreenSwitch())); + updateState(DEVICE_CHANNEL_WATER_LACKS, OnOffType.from(humidifierStatus.result.result.getWaterLacksState())); + updateState(DEVICE_CHANNEL_WATER_TANK_LIFTED, + OnOffType.from(humidifierStatus.result.result.getWaterTankLifted())); + updateState(DEVICE_CHANNEL_STOP_AT_TARGET, OnOffType.from(humidifierStatus.result.result.getAutoStopSwitch())); + updateState(DEVICE_CHANNEL_HUMIDITY, + new QuantityType<>(humidifierStatus.result.result.humidity, Units.PERCENT)); + updateState(DEVICE_CHANNEL_MIST_LEVEL, new DecimalType(humidifierStatus.result.result.mistLevel)); + if (MODE_AUTO_HUMIDITY.equals(humidifierStatus.result.result.workMode)) { + humidifierStatus.result.result.workMode = MODE_AUTO; + } + updateState(DEVICE_CHANNEL_HUMIDIFIER_MODE, new StringType(humidifierStatus.result.result.workMode)); + updateState(DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY, + new QuantityType<>(humidifierStatus.result.result.targetHumidity, Units.PERCENT)); + updateState(DEVICE_CHANNEL_ERROR_CODE, new DecimalType(humidifierStatus.result.result.errorCode)); + updateState(DEVICE_CHANNEL_AF_SCHEDULES_COUNT, new DecimalType(humidifierStatus.result.result.scheduleCount)); + if (humidifierStatus.result.result.timerRemain > 0) { + updateState(DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, new DateTimeType(LocalDateTime.now() + .plus(humidifierStatus.result.result.timerRemain, ChronoUnit.MINUTES).toString())); + } else { + updateState(DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, new DateTimeItem("nullEnforcements").getState()); + } + } } 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 index ab6d8ac8c0486..b75470b85ab05 100644 --- 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 @@ -19,8 +19,11 @@ import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.validation.constraints.NotNull; @@ -29,7 +32,12 @@ 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.requests.VeSyncRequestV1SetLevel; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestV1SetMode; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestV1SetStatus; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncResponse; import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassPurifierStatus; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2Ver2BypassPurifierStatus; import org.openhab.binding.vesync.internal.dto.responses.v1.VeSyncV1AirPurifierDeviceDetailsResponse; import org.openhab.core.cache.ExpiringCache; import org.openhab.core.library.items.DateTimeItem; @@ -55,6 +63,7 @@ * @author David Goodyear - Initial contribution */ @NonNullByDefault +@SuppressWarnings("serial") public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler { public static final String DEV_TYPE_FAMILY_AIR_PURIFIER = "LAP"; @@ -68,27 +77,54 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler { public static final String DEV_FAMILY_PUR_131S = "131S"; - public static final VeSyncDeviceMetadata CORE200S = new VeSyncDeviceMetadata(DEV_FAMILY_CORE_200S, - Arrays.asList("C201S", "C202S"), List.of("Core200S")); + public static final String DEV_FAMILY_VITAL_100S = "V100S"; - public static final VeSyncDeviceMetadata CORE300S = new VeSyncDeviceMetadata(DEV_FAMILY_CORE_300S, - List.of("C301S", "C302S"), List.of("Core300S")); + public static final String DEV_FAMILY_VITAL_200S = "V200S"; - public static final VeSyncDeviceMetadata CORE400S = new VeSyncDeviceMetadata(DEV_FAMILY_CORE_400S, List.of("C401S"), - List.of("Core400S")); + private static final List FAN_MODES_WITH_PET = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP, MODE_PET); - public static final VeSyncDeviceMetadata CORE600S = new VeSyncDeviceMetadata(DEV_FAMILY_CORE_600S, List.of("C601S"), - List.of("Core600S")); + private static final List FAN_MODES_NO_PET = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP); + private static final List FAN_MODES_MAN_SLEEP = Arrays.asList(MODE_MANUAL, MODE_SLEEP); + private static final List NIGHT_LIGHTS = Arrays.asList(MODE_ON, MODE_DIM, MODE_OFF); - public static final VeSyncDeviceMetadata PUR131S = new VeSyncDeviceMetadata(DEV_FAMILY_PUR_131S, - Collections.emptyList(), Arrays.asList("LV-PUR131S", "LV-RH131S")); + private static final List NO_NIGHT_LIGHTS = Collections.emptyList(); + public static final VeSyncDevicePurifierMetadata CORE200S = new VeSyncDevicePurifierMetadata(1, + DEV_FAMILY_CORE_200S, Arrays.asList("C201S", "C202S"), List.of("Core200S"), FAN_MODES_MAN_SLEEP, 1, 3, + NIGHT_LIGHTS); - public static final List SUPPORTED_MODEL_FAMILIES = Arrays.asList(CORE600S, CORE400S, - CORE300S, CORE200S, PUR131S); + public static final VeSyncDevicePurifierMetadata CORE300S = new VeSyncDevicePurifierMetadata(1, + DEV_FAMILY_CORE_300S, List.of("C301S", "C302S"), List.of("Core300S"), FAN_MODES_MAN_SLEEP, 1, 3, + NIGHT_LIGHTS); - 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); + public static final VeSyncDevicePurifierMetadata CORE400S = new VeSyncDevicePurifierMetadata(1, + DEV_FAMILY_CORE_400S, List.of("C401S"), List.of("Core400S"), FAN_MODES_NO_PET, 1, 4, NO_NIGHT_LIGHTS); + + public static final VeSyncDevicePurifierMetadata CORE600S = new VeSyncDevicePurifierMetadata(1, + DEV_FAMILY_CORE_600S, List.of("C601S"), List.of("Core600S"), FAN_MODES_NO_PET, 1, 4, NO_NIGHT_LIGHTS); + + public static final VeSyncDevicePurifierMetadata VITAL100S = new VeSyncDevicePurifierMetadata(2, + DEV_FAMILY_VITAL_100S, List.of("V102S"), Collections.emptyList(), FAN_MODES_NO_PET, 1, 5, NO_NIGHT_LIGHTS); + + public static final VeSyncDevicePurifierMetadata VITAL200S = new VeSyncDevicePurifierMetadata(2, + DEV_FAMILY_VITAL_200S, List.of("V201S"), Collections.emptyList(), FAN_MODES_WITH_PET, 1, 5, + NO_NIGHT_LIGHTS); + + public static final VeSyncDevicePurifierMetadata PUR131S = new VeSyncDevicePurifierMetadata(1, DEV_FAMILY_PUR_131S, + Collections.emptyList(), Arrays.asList("LV-PUR131S", "LV-RH131S"), FAN_MODES_NO_PET, 1, 3, NO_NIGHT_LIGHTS); + + public static final Map DEV_FAMILY_HUMIDIFER_MAP = new HashMap() { + { + put(PUR131S.deviceFamilyName, PUR131S); + put(CORE200S.deviceFamilyName, CORE200S); + put(CORE300S.deviceFamilyName, CORE300S); + put(CORE400S.deviceFamilyName, CORE400S); + put(CORE600S.deviceFamilyName, CORE600S); + put(VITAL100S.deviceFamilyName, VITAL100S); + put(VITAL200S.deviceFamilyName, VITAL200S); + } + }; + public static final List SUPPORTED_MODEL_FAMILIES = DEV_FAMILY_HUMIDIFER_MAP.values().stream() + .collect(Collectors.toList()); private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirPurifierHandler.class); @@ -114,16 +150,28 @@ public void initialize() { switch (deviceFamily) { case DEV_FAMILY_CORE_600S: case DEV_FAMILY_CORE_400S: - toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT }; + toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT, DEVICE_CHANNEL_AF_LIGHT_DETECTION, + DEVICE_CHANNEL_AF_LIGHT_DETECTED }; break; case DEV_FAMILY_PUR_131S: 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 }; + DEVICE_CHANNEL_AIRQUALITY_PM25, DEVICE_CHANNEL_AF_SCHEDULES_COUNT, + DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER, DEVICE_CHANNEL_ERROR_CODE, + DEVICE_CHANNEL_CHILD_LOCK_ENABLED, DEVICE_CHANNEL_AF_LIGHT_DETECTION, + DEVICE_CHANNEL_AF_LIGHT_DETECTED }; + break; + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + toRemove = new String[] { DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, DEVICE_CHANNEL_AF_SCHEDULES_COUNT, + DEVICE_CHANNEL_AF_NIGHT_LIGHT, DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF, + DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER, DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE, + DEVICE_CHANNEL_AF_LIGHT_DETECTION, DEVICE_CHANNEL_AF_LIGHT_DETECTED, + DEVICE_CHANNEL_ERROR_CODE }; break; default: - toRemove = new String[] { DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, DEVICE_CHANNEL_AF_SCHEDULES_COUNT }; + toRemove = new String[] { DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, DEVICE_CHANNEL_AF_SCHEDULES_COUNT, + DEVICE_CHANNEL_AF_LIGHT_DETECTION, DEVICE_CHANNEL_AF_LIGHT_DETECTED }; } } return toRemove; @@ -164,111 +212,153 @@ public void handleCommand(final ChannelUID channelUID, final Command command) { if (deviceFamily == null) { return; } + final String deviceUuid = getThing().getProperties().get(DEVICE_PROP_DEVICE_UUID); + if (deviceUuid == null) { + return; + } + final VeSyncDevicePurifierMetadata devContraints = DEV_FAMILY_HUMIDIFER_MAP.get(deviceFamily); + if (devContraints == null) { + logger.warn("Could not find device family for {} during handleCommand", deviceFamily); + 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)); + switch (deviceFamily) { + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + sendV2BypassControlCommand(DEVICE_SET_SWITCH, + new VeSyncRequestManagedDeviceBypassV2.SetPowerPayload( + command.equals(OnOffType.ON), 0)); + break; + case DEV_FAMILY_PUR_131S: + sendV1ControlCommand("131airPurifier/v1/device/deviceStatus", + new VeSyncRequestV1SetStatus(deviceUuid, + command.equals(OnOffType.ON) ? "on" : "off")); + break; + default: + 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))); + switch (deviceFamily) { + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + sendV2BypassControlCommand(DEVICE_SET_DISPLAY, + new VeSyncRequestManagedDeviceBypassV2.SetScreenSwitchPayload( + command.equals(OnOffType.ON))); + break; + case DEV_FAMILY_PUR_131S: + sendV1ControlCommand("131airPurifier/v1/device/updateScreen", + new VeSyncRequestV1SetStatus(deviceUuid, + command.equals(OnOffType.ON) ? "on" : "off")); + break; + default: + sendV2BypassControlCommand(DEVICE_SET_DISPLAY, + new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON))); + + break; + } break; case DEVICE_CHANNEL_CHILD_LOCK_ENABLED: - sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK, - new VeSyncRequestManagedDeviceBypassV2.SetChildLock(command.equals(OnOffType.ON))); + switch (deviceFamily) { + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK, + new VeSyncRequestManagedDeviceBypassV2.SetChildLockPayload( + command.equals(OnOffType.ON))); + break; + default: + sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK, + new VeSyncRequestManagedDeviceBypassV2.SetChildLock( + command.equals(OnOffType.ON))); + break; + } + break; + case DEVICE_CHANNEL_AF_LIGHT_DETECTION: + sendV2BypassControlCommand(DEVICE_SET_LIGHT_DETECTION, + new VeSyncRequestManagedDeviceBypassV2.SetLightDetectionPayload( + 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(); + + if (!devContraints.isFanModeSupported(targetFanMode)) { + logger.warn("Fan mode command for \"{}\" is not valid in the ({}) API possible options {}", + command, devContraints.deviceFamilyName, String.join(",", devContraints.fanModes)); + pollForUpdate(); + return; + } switch (deviceFamily) { - case DEV_FAMILY_CORE_600S: - case DEV_FAMILY_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; - } + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetWorkModePayload(targetFanMode)); break; - case DEV_FAMILY_CORE_200S: - case DEV_FAMILY_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; - } + case DEV_FAMILY_PUR_131S: + sendV1ControlCommand("131airPurifier/v1/device/updateMode", + new VeSyncRequestV1SetMode(deviceUuid, targetFanMode)); break; + default: + sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetMode(targetFanMode)); } - - sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, - new VeSyncRequestManagedDeviceBypassV2.SetMode(targetFanMode)); break; case DEVICE_CHANNEL_AF_NIGHT_LIGHT: final String targetNightLightMode = command.toString().toLowerCase(); - switch (deviceFamily) { - case DEV_FAMILY_CORE_600S: - case DEV_FAMILY_CORE_400S: - logger.warn("Core400S API does not support night light"); - return; - case DEV_FAMILY_CORE_200S: - case DEV_FAMILY_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; + if (!devContraints.isNightLightModeSupported(targetNightLightMode)) { + logger.warn( + "Night light mode command for \"{}\" is not valid in the ({}) API possible options {}", + command, devContraints.deviceFamilyName, + String.join(",", devContraints.nightLightModes)); + pollForUpdate(); + return; } + sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT, + new VeSyncRequestManagedDeviceBypassV2.SetNightLight(targetNightLightMode)); 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; + if (!devContraints.isFanSpeedSupported(requestedLevel)) { + logger.warn("Fan speed command for \"{}\" is not valid ({}) API possible options {} -> {}", + command, devContraints.deviceFamilyName, String.valueOf(devContraints.minFanSpeed), + String.valueOf(devContraints.maxFanSpeed)); + requestedLevel = requestedLevel < devContraints.minFanSpeed ? devContraints.minFanSpeed + : devContraints.maxFanSpeed; } - switch (deviceFamily) { - case DEV_FAMILY_CORE_600S: - case DEV_FAMILY_CORE_400S: - if (requestedLevel > 4) { - logger.warn( - "Fan speed command greater than 4 - adjusting to 4 as the valid (Core400S) API value"); - requestedLevel = 4; - } + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetWorkModePayload(MODE_MANUAL)); + sendV2BypassControlCommand(DEVICE_SET_LEVEL, + new VeSyncRequestManagedDeviceBypassV2.SetManualSpeedLevelPayload( + requestedLevel)); break; - case DEV_FAMILY_CORE_200S: - case DEV_FAMILY_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; - } + case DEV_FAMILY_PUR_131S: + sendV1ControlCommand("131airPurifier/v1/device/updateMode", + new VeSyncRequestV1SetMode(deviceUuid, MODE_MANUAL), false); + sendV1ControlCommand("131airPurifier/v1/device/updateSpeed", + new VeSyncRequestV1SetLevel(deviceUuid, requestedLevel)); break; + default: + sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false); + sendV2BypassControlCommand(DEVICE_SET_LEVEL, + new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, + DEVICE_LEVEL_TYPE_WIND, requestedLevel)); } - - sendV2BypassControlCommand(DEVICE_SET_LEVEL, - new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_WIND, - requestedLevel)); break; } } else if (command instanceof RefreshType) { @@ -285,17 +375,10 @@ protected void pollForDeviceData(final ExpiringCache cachedResponse) { if (deviceFamily == null) { return; } - - switch (deviceFamily) { - case DEV_FAMILY_CORE_600S: - case DEV_FAMILY_CORE_400S: - case DEV_FAMILY_CORE_300S: - case DEV_FAMILY_CORE_200S: - processV2BypassPoll(cachedResponse); - break; - case DEV_FAMILY_PUR_131S: - processV1AirPurifierPoll(cachedResponse); - break; + if (!DEV_FAMILY_PUR_131S.equals(deviceFamily)) { + processV2BypassPoll(cachedResponse); + } else { + processV1AirPurifierPoll(cachedResponse); } } @@ -312,7 +395,7 @@ private void processV1AirPurifierPoll(final ExpiringCache cachedResponse boolean cachedDataUsed = response != null; if (response == null) { logger.trace("Requesting fresh response"); - response = sendV1Command("POST", "https://smartapi.vesync.com/131airPurifier/v1/device/deviceDetail", + response = sendV1Command("131airPurifier/v1/device/deviceDetail", new VeSyncRequestV1ManagedDeviceDetails(deviceUuid)); } else { logger.trace("Using cached response {}", response); @@ -353,11 +436,22 @@ private void processV1AirPurifierPoll(final ExpiringCache cachedResponse 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())); + updateState(DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING, + new QuantityType<>(purifierStatus.filter.getPercent(), Units.PERCENT)); } private void processV2BypassPoll(final ExpiringCache cachedResponse) { + final String deviceFamily = getThing().getProperties().get(DEVICE_PROP_DEVICE_FAMILY); + + final VeSyncDevicePurifierMetadata devContraints = DEV_FAMILY_HUMIDIFER_MAP.get(deviceFamily); + if (devContraints == null) { + logger.warn("Could not find device family for {} during handleCommand", deviceFamily); + return; + } + String response; - VeSyncV2BypassPurifierStatus purifierStatus; + VeSyncResponse purifierStatus = null; + synchronized (pollLock) { response = cachedResponse.getValue(); boolean cachedDataUsed = response != null; @@ -372,8 +466,11 @@ private void processV2BypassPoll(final ExpiringCache cachedResponse) { if (response.equals(EMPTY_STRING)) { return; } - - purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassPurifierStatus.class); + if (devContraints.protocolV2Version == 2) { + purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2Ver2BypassPurifierStatus.class); + } else { + purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassPurifierStatus.class); + } if (purifierStatus == null) { return; @@ -393,6 +490,14 @@ private void processV2BypassPoll(final ExpiringCache cachedResponse) { updateStatus(ThingStatus.ONLINE); } + if (devContraints.protocolV2Version == 2) { + parseV2Ver2Poll((VeSyncV2Ver2BypassPurifierStatus) purifierStatus); + } else { + parseV2Ver1Poll((VeSyncV2BypassPurifierStatus) purifierStatus); + } + } + + private void parseV2Ver1Poll(final VeSyncV2BypassPurifierStatus purifierStatus) { if (!"0".equals(purifierStatus.result.getCode())) { logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier"); return; @@ -409,13 +514,10 @@ private void processV2BypassPoll(final ExpiringCache cachedResponse) { 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)); @@ -436,4 +538,28 @@ private void processV2BypassPoll(final ExpiringCache cachedResponse) { updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new DecimalType(purifierStatus.result.result.nightLight)); } } + + private void parseV2Ver2Poll(final VeSyncV2Ver2BypassPurifierStatus purifierStatus) { + 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.getPowerSwitch())); + updateState(DEVICE_CHANNEL_CHILD_LOCK_ENABLED, + OnOffType.from(purifierStatus.result.result.getChildLockSwitch())); + updateState(DEVICE_CHANNEL_AIRQUALITY_BASIC, new DecimalType(purifierStatus.result.result.airQuality)); + updateState(DEVICE_CHANNEL_AIRQUALITY_PM25, + new QuantityType<>(purifierStatus.result.result.PM25, Units.MICROGRAM_PER_CUBICMETRE)); + updateState(DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING, + new QuantityType<>(purifierStatus.result.result.filterLifePercent, Units.PERCENT)); + updateState(DEVICE_CHANNEL_AF_LIGHT_DETECTION, + OnOffType.from(purifierStatus.result.result.getLightDetectionSwitch())); + updateState(DEVICE_CHANNEL_AF_LIGHT_DETECTED, + OnOffType.from(purifierStatus.result.result.getEnvironmentLightState())); + updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(purifierStatus.result.result.getScreenSwitch())); + updateState(DEVICE_CHANNEL_FAN_MODE_ENABLED, new StringType(purifierStatus.result.result.workMode)); + updateState(DEVICE_CHANNEL_FAN_SPEED_ENABLED, new DecimalType(purifierStatus.result.result.fanSpeedLevel)); + updateState(DEVICE_CHANNEL_ERROR_CODE, new DecimalType(purifierStatus.result.result.errorCode)); + } } diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceHumidifierMetadata.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceHumidifierMetadata.java new file mode 100644 index 0000000000000..f0401924e98a7 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceHumidifierMetadata.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handlers; + +import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.MODE_AUTO; +import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.MODE_AUTO_HUMIDITY; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link VeSyncDeviceHumidifierMetadata} class contains the definition for the control of humidifer device types. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VeSyncDeviceHumidifierMetadata extends VeSyncDeviceMetadata { + + public VeSyncDeviceHumidifierMetadata(final int v2version, final String deviceFamilyName, + final List deviceGenerations, final List nonStandardIds, final List fanModes, + final int targetMinMistLevel, final int targetMaxMistLevel, final int targetMinWarmMistLevel, + final int targetMaxWarmMistLevel, final boolean remapsAutoToHumidity, List nightLightModes) { + super(deviceFamilyName, deviceGenerations, nonStandardIds); + this.fanModes = fanModes; + this.targetMinMistLevel = targetMinMistLevel; + this.targetMaxMistLevel = targetMaxMistLevel; + this.targetMinWarmMistLevel = targetMinWarmMistLevel; + this.targetMaxWarmMistLevel = targetMaxWarmMistLevel; + this.remapsAutoToHumidity = remapsAutoToHumidity; + this.nightLightModes = nightLightModes; + this.protocolV2Version = v2version; + } + + public final int protocolV2Version; + + /** + * The fan modes supported by this generation of device + */ + public final List fanModes; + + /** + * The minimum target mist level supported + */ + public final int targetMinMistLevel; + + /** + * The maximum target mist level supported + */ + public final int targetMaxMistLevel; + + public final boolean isTargetMistLevelSupported(final int target) { + return target >= targetMinMistLevel && target <= targetMaxMistLevel; + } + + /** + * The minimum target mist level supported + */ + public final int targetMinWarmMistLevel; + + /** + * The maximum target mist level supported + */ + public final int targetMaxWarmMistLevel; + + public final boolean isTargetWramMistLevelSupported(final int target) { + return target >= targetMinWarmMistLevel && target <= targetMaxWarmMistLevel; + } + + /** + * Stores whether auto in openhab is humidity mode in the protocol + */ + public final boolean remapsAutoToHumidity; + + public String getProtocolMode(final String mode) { + if (!remapsAutoToHumidity) { + return mode; + } else { + if (mode.equals(MODE_AUTO)) { + return MODE_AUTO_HUMIDITY; + } + return mode; + } + } + + public List nightLightModes; +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDevicePurifierMetadata.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDevicePurifierMetadata.java new file mode 100644 index 0000000000000..0c1c1568439ff --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDevicePurifierMetadata.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handlers; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link VeSyncDevicePurifierMetadata} class contains the definition for the control of humidifer device types. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VeSyncDevicePurifierMetadata extends VeSyncDeviceMetadata { + + public VeSyncDevicePurifierMetadata(final int v2version, final String deviceFamilyName, + final List deviceGenerations, final List nonStandardIds, final List fanModes, + final int minFanSpeed, final int maxFanSpeed, final List nightLightModes) { + super(deviceFamilyName, deviceGenerations, nonStandardIds); + this.fanModes = fanModes; + this.minFanSpeed = minFanSpeed; + this.maxFanSpeed = maxFanSpeed; + this.nightLightModes = nightLightModes; + this.protocolV2Version = v2version; + } + + public final int protocolV2Version; + + /** + * The fan modes supported by this generation of device + */ + public final List fanModes; + + /** + * The minimum fan speed supported + */ + public final int minFanSpeed; + + /** + * The maximum fan speed supported + */ + public final int maxFanSpeed; + + /** + * The night light supported by this generation of device + */ + public final List nightLightModes; + + public final boolean isFanModeSupported(final String fanMode) { + return fanModes.contains(fanMode); + } + + public final boolean isFanSpeedSupported(final int speed) { + return speed >= minFanSpeed && speed <= maxFanSpeed; + } + + public final boolean isNightLightModeSupported(final String nightLightMode) { + return nightLightModes.contains(nightLightMode); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/i18n/vesync.properties b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/i18n/vesync.properties index 240a2cf0e09a8..466c8930f680c 100644 --- a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/i18n/vesync.properties +++ b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/i18n/vesync.properties @@ -45,6 +45,7 @@ channel-type.vesync.airPurifierModeType.description = The operating mode the air channel-type.vesync.airPurifierModeType.state.option.auto = Auto channel-type.vesync.airPurifierModeType.state.option.manual = Manual Fan Control channel-type.vesync.airPurifierModeType.state.option.sleep = Sleeping Auto +channel-type.vesync.airPurifierModeType.state.option.pet = Pet Auto channel-type.vesync.airQualityPM25.label = Air Quality PPM2.5 channel-type.vesync.airQualityPM25.description = Indicator of current air quality channel-type.vesync.deviceAFConfigAutoPrefRoomSizeType.label = Config: Room size @@ -66,6 +67,10 @@ channel-type.vesync.deviceAFNightLight.state.option.off = Off channel-type.vesync.deviceAFTimerExpiry.label = Auto Off Expiry channel-type.vesync.deviceAFTimerExpiry.description = The time when the auto off timer will be reached channel-type.vesync.deviceAFTimerExpiry.state.pattern = %1$tF %1$tR +channel-type.vesync.deviceAFLightDetection.label = Light Detection +channel-type.vesync.deviceAFLightDetection.description = If the devices light detection is enabled +channel-type.vesync.deviceAFLightDetected.label = Light Detected +channel-type.vesync.deviceAFLightDetected.description = Indicator if the device detects light channel-type.vesync.deviceAirQualityBasicType.label = Air Quality channel-type.vesync.deviceAirQualityBasicType.description = System representation of air quality channel-type.vesync.deviceAutomaticStopReachTargetType.label = Stop @ Set Point 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 index e1ffdac5c496e..260e65dc06237 100644 --- 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 @@ -64,6 +64,9 @@ + + + @@ -109,6 +112,8 @@ + + @@ -169,6 +174,7 @@ + @@ -221,6 +227,20 @@ Configuration: If the devices display is enabled forever + + Switch + + If the devices light detection is enabled + + + + + Switch + + Indicator if the device detects light + + + String @@ -332,5 +352,4 @@ -