diff --git a/bundles/org.openhab.binding.vesync/README.md b/bundles/org.openhab.binding.vesync/README.md index 9ffcd7d1965e9..73ed7af38ebe5 100644 --- a/bundles/org.openhab.binding.vesync/README.md +++ b/bundles/org.openhab.binding.vesync/README.md @@ -4,13 +4,13 @@ Its current support is for the Air Purifiers & Humidifer's branded as Levoit whi ## Verified Models -Air Filtering models supported are Core300S, Core400S. -Air Humidifier models supported are Dual 200S, Classic 300S, 600S, OasisMist Smart Humidifier +Air Filtering models verified are Core300S, Core400S, Vital 100S. +Air Humidifier models verified are Dual 200S, Classic 300S, 600S, OasisMist Smart Humidifier ## 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 @@ -24,7 +24,7 @@ This binding supports the follow thing types: This binding was developed from the great work in the listed projects. -The only Air Filter unit it has been tested against is the Core400S unit, **I'm looking for others to confirm** my queries regarding **the Core200S and Core300S** units. +The only Air Filter unit it has been tested against are the Core400S unit and Vital 100S, **I'm looking for others to confirm** my queries regarding **the Core200S and Core300S** units. The **Classic 300S Humidifier** has been tested, and **600S with current warm mode restrictions**. ## Discovery @@ -66,43 +66,49 @@ 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 of schedules which are configured | 600S, 400S | | one | +| 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 | | one | ### 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 | Unit | +|----------------------------|----------------------|---------------------------------------------------------------|------------------------------------------------------|---------------------|------| +| **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] | one | +| **mistLevel** | Number:Dimensionless | The current mist level set | 200S, Dual200S, 600S, OasisMist, OasisMist1000 | [1...3] | one | +| **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] | one | +| errorCode | Number:Dimensionless | The error code reported by the device | OasisMist1000 | | one | +| timerExpiry | DateTime | The expected expiry time of the current timer | OasisMist1000 | | | +| schedulesCount | Number:Dimensionless | The number schedules configured | OasisMist1000 | | one | ## Full Example @@ -129,7 +135,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 +143,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 +153,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 +161,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 +229,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,6 +245,26 @@ 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) @@ -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 9c2880a4105fd..48850f4444a53 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/VeSyncHandlerFactory.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncHandlerFactory.java index eeb63d8f85985..58c30449af5a3 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncHandlerFactory.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncHandlerFactory.java @@ -18,11 +18,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; -import org.openhab.binding.vesync.internal.api.IHttpClientProvider; import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler; import org.openhab.binding.vesync.internal.handlers.VeSyncDeviceAirHumidifierHandler; import org.openhab.binding.vesync.internal.handlers.VeSyncDeviceAirPurifierHandler; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; @@ -30,6 +30,7 @@ import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -41,12 +42,23 @@ */ @NonNullByDefault @Component(configurationPid = "binding.vesync", service = ThingHandlerFactory.class) -public class VeSyncHandlerFactory extends BaseThingHandlerFactory implements IHttpClientProvider { +public class VeSyncHandlerFactory extends BaseThingHandlerFactory { private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_AIR_PURIFIER, THING_TYPE_AIR_HUMIDIFIER); - private @Nullable HttpClient httpClientRef = null; + private final HttpClientFactory httpClientFactory; + private final TranslationProvider translationProvider; + private final LocaleProvider localeProvider; + + @Activate + public VeSyncHandlerFactory(@Reference HttpClientFactory httpClientFactory, + @Reference TranslationProvider translationProvider, @Reference LocaleProvider localeProvider) { + super(); + this.httpClientFactory = httpClientFactory; + this.translationProvider = translationProvider; + this.localeProvider = localeProvider; + } @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { @@ -58,23 +70,13 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (VeSyncDeviceAirPurifierHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new VeSyncDeviceAirPurifierHandler(thing); + return new VeSyncDeviceAirPurifierHandler(thing, translationProvider, localeProvider); } else if (VeSyncDeviceAirHumidifierHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new VeSyncDeviceAirHumidifierHandler(thing); + return new VeSyncDeviceAirHumidifierHandler(thing, translationProvider, localeProvider); } else if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { - return new VeSyncBridgeHandler((Bridge) thing, this); + return new VeSyncBridgeHandler((Bridge) thing, httpClientFactory, translationProvider, localeProvider); } return null; } - - @Reference - protected void setHttpClientFactory(HttpClientFactory httpClientFactory) { - httpClientRef = httpClientFactory.getCommonHttpClient(); - } - - @Override - public @Nullable HttpClient getHttpClient() { - return httpClientRef; - } } diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/IHttpClientProvider.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/IHttpClientProvider.java deleted file mode 100644 index 1698126e1e49b..0000000000000 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/IHttpClientProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.vesync.internal.api; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; - -/** - * @author David Goodyear - Initial contribution - */ -@NonNullByDefault -public interface IHttpClientProvider { - @Nullable - HttpClient getHttpClient(); -} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/VeSyncV2ApiHelper.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/VeSyncV2ApiHelper.java index 801ce26db004b..fa654ba8b4344 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 @@ -34,6 +34,7 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; import org.openhab.binding.vesync.internal.VeSyncConstants; import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest; import org.openhab.binding.vesync.internal.dto.requests.VeSyncLoginCredentials; @@ -58,10 +59,12 @@ public class VeSyncV2ApiHelper { private final Logger logger = LoggerFactory.getLogger(VeSyncV2ApiHelper.class); - private @NonNullByDefault({}) HttpClient httpClient; + private static final int RESPONSE_TIMEOUT_SEC = 5; private volatile @Nullable VeSyncUserSession loggedInSession; + private @NonNullByDefault({}) HttpClient httpClient; + private Map macLookup; public VeSyncV2ApiHelper() { @@ -154,6 +157,7 @@ public String reqV2Authorized(final String url, final String macId, final VeSync } veSyncRequestManagedDeviceBypassV2.cid = deviceData.cid; veSyncRequestManagedDeviceBypassV2.configModule = deviceData.configModule; + veSyncRequestManagedDeviceBypassV2.configModel = deviceData.configModule; veSyncRequestManagedDeviceBypassV2.deviceRegion = deviceData.deviceRegion; } return reqV1Authorized(url, requestData); @@ -167,16 +171,18 @@ 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).timeout(RESPONSE_TIMEOUT_SEC, + TimeUnit.SECONDS); // 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"); - ContentResponse response = request.timeout(5, TimeUnit.SECONDS).send(); + ContentResponse response = request.send(); if (response.getStatus() == HttpURLConnection.HTTP_OK) { VeSyncResponse commResponse = VeSyncConstants.GSON.fromJson(response.getContentAsString(), VeSyncResponse.class); @@ -220,7 +226,8 @@ public void updateBridgeData(final VeSyncBridgeHandler bridge) { private VeSyncLoginResponse processLogin(String username, String password, String timezone) throws AuthenticationException { try { - Request request = httpClient.POST(V1_LOGIN_ENDPOINT); + Request request = httpClient.newRequest(V1_LOGIN_ENDPOINT).method(HttpMethod.POST) + .timeout(RESPONSE_TIMEOUT_SEC, TimeUnit.SECONDS); // No headers for login request.content(new StringContentProvider( @@ -228,7 +235,7 @@ private VeSyncLoginResponse processLogin(String username, String password, Strin request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8"); - ContentResponse response = request.timeout(5, TimeUnit.SECONDS).send(); + ContentResponse response = request.send(); if (response.getStatus() == HttpURLConnection.HTTP_OK) { VeSyncLoginResponse loginResponse = VeSyncConstants.GSON.fromJson(response.getContentAsString(), VeSyncLoginResponse.class); 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 c6925f727f0f4..741ff024bf2fe 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 2b17baed81605..04e6dc67b44f6 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 org.eclipse.jetty.http.HttpMethod; + import com.google.gson.annotations.SerializedName; /** @@ -21,6 +23,8 @@ */ public class VeSyncRequest { + public transient HttpMethod httpMethod; + @SerializedName("timeZone") public String timeZone = "America/New_York"; @@ -42,7 +46,11 @@ public class VeSyncRequest { @SerializedName("method") public String method; + @SerializedName("deviceId") + public String deviceId; + 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 70f6feb1de71f..7aec7ab8844a2 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 @@ -34,6 +34,9 @@ public class VeSyncRequestManagedDeviceBypassV2 extends VeSyncAuthenticatedReque @SerializedName("configModule") public String configModule = ""; + @SerializedName("configModel") + public String configModel = ""; + @SerializedName("payload") public VesyncManagedDeviceBase payload = new VesyncManagedDeviceBase(); @@ -55,6 +58,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..77a6f8c472b5a --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1Command.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import org.eclipse.jetty.http.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 d0cd5a8e32477..e0f2bb8edbab1 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,8 +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 - * API. + * 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..aa132cb782fe5 --- /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-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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..92b8eff8c9a7b --- /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-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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..fc0a51a7c0f78 --- /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-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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 9d8c0e8b851aa..2379471083bd6 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..2820b86321278 --- /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-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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..863d1ca68ccc9 --- /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-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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 7dae44c14bf80..ae1e190d9f3ef 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 44a8288b678a7..c79e2fbfc8cf7 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 @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -32,11 +33,14 @@ 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; import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException; import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; @@ -47,6 +51,10 @@ 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.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,8 +96,23 @@ public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler { @Nullable ScheduledFuture readbackPollTask = null; - public VeSyncBaseDeviceHandler(Thing thing) { + private final TranslationProvider translationProvider; + private final LocaleProvider localeProvider; + private final Bundle bundle; + + private String deviceId = ""; + + public VeSyncBaseDeviceHandler(Thing thing, @Reference TranslationProvider translationProvider, + @Reference LocaleProvider localeProvider) { super(thing); + this.translationProvider = translationProvider; + this.localeProvider = localeProvider; + this.bundle = FrameworkUtil.getBundle(getClass()); + } + + public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) { + String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments); + return Objects.nonNull(result) ? result : key; } protected @Nullable Channel findChannelById(final String channelGroupId) { @@ -151,12 +174,7 @@ protected boolean isDeviceOnline() { if (bridgeHandler instanceof VeSyncBridgeHandler veSyncBridgeHandler) { @Nullable VeSyncManagedDeviceBase metadata = veSyncBridgeHandler.api.getMacLookupMap().get(deviceLookupKey); - - if (metadata == null) { - return false; - } - - return ("online".equals(metadata.connectionStatus)); + return metadata != null && "online".equals(metadata.connectionStatus); } return false; } @@ -175,6 +193,8 @@ public void updateDeviceMetaData() { newProps = getMetadataProperities(metadata); + deviceId = metadata.getUuid(); + // Refresh the device -> protocol mapping deviceLookupKey = getValidatedIdString(); @@ -404,13 +424,26 @@ protected final String sendV2BypassControlCommand(final String method, protected final String sendV2BypassControlCommand(final String method, final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload, final boolean readbackDevice) { final String result = sendV2BypassCommand(method, payload); - if (!result.equals(EMPTY_STRING) && readbackDevice) { + if (!EMPTY_STRING.equals(result) && readbackDevice) { performReadbackPoll(); } 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 (!EMPTY_STRING.equals(result) && 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; @@ -422,6 +455,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"); @@ -455,6 +489,7 @@ protected final String sendV2BypassCommand(final String method, VeSyncRequestManagedDeviceBypassV2 readReq = new VeSyncRequestManagedDeviceBypassV2(); readReq.payload.method = method; readReq.payload.data = payload; + readReq.deviceId = deviceId; try { if (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)) { @@ -547,4 +582,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/VeSyncBridgeHandler.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBridgeHandler.java index b760b2e00fd51..4e5059a954c82 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBridgeHandler.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBridgeHandler.java @@ -17,6 +17,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; @@ -26,8 +27,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration; -import org.openhab.binding.vesync.internal.api.IHttpClientProvider; import org.openhab.binding.vesync.internal.api.VeSyncV2ApiHelper; import org.openhab.binding.vesync.internal.discovery.DeviceMetaDataUpdatedHandler; import org.openhab.binding.vesync.internal.discovery.VeSyncDiscoveryService; @@ -36,6 +37,9 @@ import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession; import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -46,6 +50,9 @@ import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,19 +69,31 @@ public class VeSyncBridgeHandler extends BaseBridgeHandler implements VeSyncClie private static final int DEFAULT_DEVICE_SCAN_RECOVERY_INTERVAL = 60; private static final int DEFAULT_DEVICE_SCAN_DISABLED = -1; - private final Logger logger = LoggerFactory.getLogger(VeSyncBridgeHandler.class); - - private @Nullable ScheduledFuture backgroundDiscoveryPollingJob; + private volatile int backgroundScanTime = -1; protected final VeSyncV2ApiHelper api = new VeSyncV2ApiHelper(); - private IHttpClientProvider httpClientProvider; - - private volatile int backgroundScanTime = -1; + private final Logger logger = LoggerFactory.getLogger(VeSyncBridgeHandler.class); private final Object scanConfigLock = new Object(); - public VeSyncBridgeHandler(Bridge bridge, @NotNull IHttpClientProvider httpClientProvider) { + private final TranslationProvider translationProvider; + private final LocaleProvider localeProvider; + private final Bundle bundle; + + private @Nullable ScheduledFuture backgroundDiscoveryPollingJob; + private HttpClient httpClient; + + public VeSyncBridgeHandler(Bridge bridge, @Reference HttpClientFactory httpClientFactory, + @Reference TranslationProvider translationProvider, @Reference LocaleProvider localeProvider) { super(bridge); - this.httpClientProvider = httpClientProvider; + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.translationProvider = translationProvider; + this.localeProvider = localeProvider; + this.bundle = FrameworkUtil.getBundle(getClass()); + } + + public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) { + String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments); + return Objects.nonNull(result) ? result : key; } public ThingUID getUID() { @@ -145,7 +164,8 @@ public void runDeviceScanSequenceNoAuthErrors() { runDeviceScanSequence(); updateStatus(ThingStatus.ONLINE); } catch (AuthenticationException ae) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check login credentials"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + getLocalizedText("bridge.offline.check-credentials")); } } @@ -198,7 +218,7 @@ public Collection> getServices() { @Override public void initialize() { - api.setHttpClient(httpClientProvider.getHttpClient()); + api.setHttpClient(httpClient); VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class); @@ -211,7 +231,8 @@ public void initialize() { runDeviceScanSequence(); updateStatus(ThingStatus.ONLINE); } catch (final AuthenticationException ae) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check login credentials"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + getLocalizedText("bridge.offline.check-credentials")); // The background scan will keep trying to authenticate in case the users credentials are updated on the // veSync servers, // to match the binding's configuration. @@ -227,7 +248,7 @@ public void dispose() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - logger.warn("Handling command for VeSync bridge handler."); + logger.warn("{}", getLocalizedText("warning.bridge.unexpected-command-call")); } public void handleNewUserSession(final @Nullable VeSyncUserSession userSessionData) { 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 fbab426ec3fb6..da048d20d4324 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,28 @@ 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.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +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; @@ -37,6 +48,7 @@ import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,11 +59,14 @@ * @author David Goodyear - Initial contribution */ @NonNullByDefault +@SuppressWarnings("serial") public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler { public static final String DEV_TYPE_FAMILY_AIR_HUMIDIFIER = "LUH"; public static final int DEFAULT_AIR_PURIFIER_POLL_RATE = 120; + private static final int MIN_TARGET_HUMIDITY = 30; + private static final int MAX_TARGET_HUMIDITY = 80; public static final String DEV_FAMILY_CLASSIC_200S = "Classic 200S"; public static final String DEV_FAMILY_CLASSIC_300S = "Classic 300S"; @@ -59,35 +74,61 @@ 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 VeSyncDeviceAirHumidifierHandler(Thing thing) { - super(thing); + 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, @Reference TranslationProvider translationProvider, + @Reference LocaleProvider localeProvider) { + super(thing, translationProvider, localeProvider); } @Override @@ -97,15 +138,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 +200,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("{}", getLocalizedText("warning.device.command-device-family-not-found", deviceFamily)); + return; + } scheduler.submit(() -> { @@ -171,47 +224,44 @@ public void handleCommand(final ChannelUID channelUID, final Command command) { new VeSyncRequestManagedDeviceBypassV2.EnabledPayload(command.equals(OnOffType.ON))); break; case DEVICE_CHANNEL_WARM_ENABLED: - logger.warn("Warm mode API is unknown in order to send the command"); + logger.warn("{}", getLocalizedText("warning.device.warm-mode-unsupported")); break; } } else if (command instanceof QuantityType quantityCommand) { switch (channelUID.getId()) { case DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY: int targetHumidity = quantityCommand.intValue(); - if (targetHumidity < 30) { - logger.warn("Target Humidity less than 30 - adjusting to 30 as the valid API value"); - targetHumidity = 30; - } else if (targetHumidity > 80) { - logger.warn("Target Humidity greater than 80 - adjusting to 80 as the valid API value"); - targetHumidity = 80; + if (targetHumidity < MIN_TARGET_HUMIDITY) { + logger.warn("{}", getLocalizedText("warning.device.humidity-under", MIN_TARGET_HUMIDITY)); + targetHumidity = MIN_TARGET_HUMIDITY; + } else if (targetHumidity > MAX_TARGET_HUMIDITY) { + logger.warn("{}", getLocalizedText("warning.device.humidity-over", MAX_TARGET_HUMIDITY)); + targetHumidity = MAX_TARGET_HUMIDITY; } 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 = quantityCommand.intValue(); + if (!devContraints.isTargetMistLevelSupported(targetMistLevel)) { + logger.warn("{}", + getLocalizedText("warning.device.mist-level-invalid", 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 +284,42 @@ 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 = quantityCommand.intValue(); + if (!devContraints.isTargetWramMistLevelSupported(targetWarmMistLevel)) { + logger.warn("{}", + getLocalizedText("warning.device.mist-level-invalid", 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)) { - logger.warn( - "Humidifier mode command for \"{}\" is not valid in the (Classic300S/600S) API possible options {}", - command, String.join(",", CLASSIC_300S_NIGHT_LIGHT_MODES)); + if (!devContraints.fanModes.contains(targetMode)) { + logger.warn("{}", getLocalizedText("warning.device.humidity-mode", 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)) { - 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)); + if (!devContraints.nightLightModes.contains(targetMode)) { + logger.warn("{}", getLocalizedText("warning.device.night-light-invalid", command, + devContraints.deviceFamilyName, String.join(",", devContraints.nightLightModes))); return; } + int targetValue; switch (targetMode) { case MODE_OFF: @@ -289,7 +348,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("{}", getLocalizedText("warning.device.poll-device-family-not-found", deviceFamily)); + return; + } + synchronized (pollLock) { response = cachedResponse.getValue(); boolean cachedDataUsed = response != null; @@ -305,7 +373,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 +397,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("{}", getLocalizedText("warning.device.unexpected-resp-for-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 +421,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 +437,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("{}", getLocalizedText("warning.device.unexpected-resp-for-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 5c9ce2028e912..c989dbbf37cdc 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,9 +32,16 @@ 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.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.library.items.DateTimeItem; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; @@ -45,6 +55,7 @@ import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,6 +66,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 +80,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_PURIFIER_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_PURIFIER_MAP.values().stream() + .collect(Collectors.toList()); private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirPurifierHandler.class); @@ -96,8 +135,9 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler { private final Object pollLock = new Object(); - public VeSyncDeviceAirPurifierHandler(Thing thing) { - super(thing); + public VeSyncDeviceAirPurifierHandler(Thing thing, @Reference TranslationProvider translationProvider, + @Reference LocaleProvider localeProvider) { + super(thing, translationProvider, localeProvider); } @Override @@ -114,16 +154,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 +216,152 @@ 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_PURIFIER_MAP.get(deviceFamily); + if (devContraints == null) { + logger.warn("{}", getLocalizedText("warning.device.command-device-family-not-found", 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("{}", getLocalizedText("warning.device.fan-mode-invalid", 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("{}", getLocalizedText("warning.device.night-light-invalid", command, + devContraints.deviceFamilyName, String.join(",", devContraints.nightLightModes))); + pollForUpdate(); + return; } + sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT, + new VeSyncRequestManagedDeviceBypassV2.SetNightLight(targetNightLightMode)); break; } } else if (command instanceof QuantityType quantityCommand) { 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 = quantityCommand.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("{}", + getLocalizedText("warning.device.fan-speed-invalid", 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 +378,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,13 +398,13 @@ 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); } - if (response.equals(EMPTY_STRING)) { + if (EMPTY_STRING.equals(response)) { return; } @@ -343,7 +429,7 @@ private void processV1AirPurifierPoll(final ExpiringCache cachedResponse } if (!"0".equals(purifierStatus.getCode())) { - logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier"); + logger.warn("{}", getLocalizedText("warning.device.unexpected-resp-for-air-purifier")); return; } @@ -353,11 +439,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_PURIFIER_MAP.get(deviceFamily); + if (devContraints == null) { + logger.warn("{}", getLocalizedText("warning.device.command-device-family-not-found", deviceFamily)); + return; + } + String response; - VeSyncV2BypassPurifierStatus purifierStatus; + VeSyncResponse purifierStatus = null; + synchronized (pollLock) { response = cachedResponse.getValue(); boolean cachedDataUsed = response != null; @@ -372,8 +469,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,8 +493,16 @@ 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"); + logger.warn("{}", getLocalizedText("warning.device.unexpected-resp-for-air-purifier")); return; } @@ -409,13 +517,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 +541,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("{}", getLocalizedText("warning.device.unexpected-resp-for-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..eb709005c2a70 --- /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-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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_AUTO.equals(mode)) { + 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/VeSyncDeviceMetadata.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceMetadata.java index 431829b633df1..5dc25bcc54d35 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceMetadata.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceMetadata.java @@ -51,13 +51,8 @@ public VeSyncDeviceMetadata(final String deviceFamilyName, final List de public final List nonStandardIds; public boolean deviceTypeIdMatches(final String deviceType, final String[] deviceTypeSegments) { - if (nonStandardIds.contains(deviceType)) { - return true; - } - if (deviceTypeSegments.length == 3) { - return deviceGenerations.contains(deviceTypeSegments[1]); - } - return false; + return nonStandardIds.contains(deviceType) + || (deviceTypeSegments.length == 3 && deviceGenerations.contains(deviceTypeSegments[1])); } public String getDeviceFamilyName() { 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..4b6c0a95bfc3d --- /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-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 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..4a640363a086d 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 @@ -58,6 +59,10 @@ channel-type.vesync.deviceAFConfigAutoScheduleCountType.label = Config: Schedule channel-type.vesync.deviceAFConfigAutoScheduleCountType.description = The current number of schedules configured channel-type.vesync.deviceAFConfigDisplayForever.label = Config: Display Forever channel-type.vesync.deviceAFConfigDisplayForever.description = Configuration: If the devices display is enabled forever +channel-type.vesync.deviceAFLightDetected.label = Light Detected +channel-type.vesync.deviceAFLightDetected.description = Indicator if the device detects light +channel-type.vesync.deviceAFLightDetection.label = Light Detection +channel-type.vesync.deviceAFLightDetection.description = If the devices light detection is enabled channel-type.vesync.deviceAFNightLight.label = Night Light channel-type.vesync.deviceAFNightLight.description = The operating mode of the night light functionality channel-type.vesync.deviceAFNightLight.state.option.on = On @@ -96,3 +101,23 @@ channel-type.vesync.warmLevel.label = Warm Level channel-type.vesync.warmLevel.description = Warm Level channel-type.vesync.warmModeEnabled.label = Warm Mode Enabled channel-type.vesync.warmModeEnabled.description = Indicator if the device is set to warm mist + +# bridge status messages + +bridge.offline.check-credentials = Check login credentials + +# warnings + +warning.bridge.unexpected-command-call = Handling command for VeSync bridge handler +warning.device.command-device-family-not-found = Could not find device family for {0} during handleCommand +warning.device.poll-device-family-not-found = Could not find device family for {0} during pollForDeviceData +warning.device.fan-mode-invalid = Fan mode command for "{0}" is not valid in the ({1}) API possible options {2} +warning.device.fan-speed-invalid = Fan speed command for "{0}" is not valid ({1}) API possible options {2} -> {3} +warning.device.mist-level-invalid = Mist level command for "{0}" is not valid ({1}) API possible options {2} -> {3} +warning.device.night-light-invalid = Night light mode command for "{0}" is not valid in the ({1}) API possible options {2} +warning.device.humidity-under = Target Humidity less than {0} - adjusting to {0} as the valid API value +warning.device.humidity-over = Target Humidity greater than {0} - adjusting to {0} as the valid API value +warning.device.humidity-mode = Humidifier mode command for {0} is not valid in the ({1}}) API possible options {2} +warning.device.warm-mode-unsupported = Warm mode API is unknown in order to send the command +warning.device.unexpected-resp-for-air-purifier = Check Thing type has been set - API gave a unexpected response for an Air Purifier +warning.device.unexpected-resp-for-air-humidifier = Check Thing type has been set - API gave a unexpected response for an Air Humidifier 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 786f2f804b524..08bd20f7010df 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 @@ -12,6 +12,7 @@ + 1 @@ -64,6 +65,8 @@ + + @@ -71,6 +74,7 @@ + 1 macId @@ -109,6 +113,8 @@ + + @@ -154,10 +160,10 @@ - Number:Dimensionless + Number:Dimensionless Indicator of the remaining filter life - + @@ -169,6 +175,7 @@ + @@ -188,14 +195,14 @@ - Number:Dimensionless + Number:Dimensionless Indicator of the current fan speed - Number:Dimensionless + Number:Dimensionless Indicator of the current error code of the device @@ -209,7 +216,7 @@ - Number:Density + Number:Density Indicator of current air quality @@ -221,6 +228,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 @@ -242,14 +263,14 @@ - Number:Dimensionless + Number:Dimensionless Room size (foot sq) for efficient auto mode - Number:Dimensionless + Number:Dimensionless The current number of schedules configured @@ -299,7 +320,7 @@ - Number:Dimensionless + Number:Dimensionless System representation of mist level @@ -326,11 +347,10 @@ - Number:Dimensionless + Number:Dimensionless Warm Level - diff --git a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-humidifier-instructions.xml b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-humidifier-instructions.xml new file mode 100644 index 0000000000000..7bb0a4239c8e6 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-humidifier-instructions.xml @@ -0,0 +1,23 @@ + + + + + + + vesync:deviceAFConfigAutoScheduleCountType + + + vesync:deviceAFTimerExpiry + + + vesync:deviceMistLevelType + + + vesync:warmLevel + + + + + diff --git a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-purifier-instructions.xml b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-purifier-instructions.xml new file mode 100644 index 0000000000000..d4f4eb24da074 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-purifier-instructions.xml @@ -0,0 +1,38 @@ + + + + + + + vesync:deviceAFLightDetection + + + vesync:deviceAFLightDetected + + + vesync:airPurifierFanLevelType + + + vesync:airPurifierModeType + + + vesync:deviceFilterLifePercentageType + + + vesync:deviceErrorCodeType + + + vesync:deviceAFConfigAutoPrefRoomSizeType + + + vesync:deviceAFConfigAutoScheduleCountType + + + vesync:airQualityPM25 + + + + +