[veSync] 131 and Vital Purifiers base support (#15296)

* [veSync] Device support enhancements

Device support enhancements
Signed-off-by: David Goodyear <david.goodyear@gmail.com>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
David Goodyear 2024-12-03 11:31:48 +00:00 committed by Ciprian Pascu
parent a75143239c
commit 546b58e0da
28 changed files with 1533 additions and 340 deletions

View File

@ -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 binding has been tested against the following Air Filter unit's: Core400S 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,50 @@ 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:Area | Config: The room size set when auto utilises the room size | 600S, 400S, 300S | | |
### AirHumidifier Thing
| Channel | Type | Description | Model's Supported | Controllable Values |
|----------------------------|----------------------|---------------------------------------------------------------|---------------------------------------|---------------------|
| **enabled** | Switch | Whether the hardware device is enabled (Switched on) | 200S, Dual200S, 300S, 600S, OasisMist | [ON, OFF] |
| **display** | Switch | Whether the display is enabled (display is shown) | 200S, Dual200S, 300S, 600S, OasisMist | [ON, OFF] |
| waterLacking | Switch | Indicator whether the unit is lacking water | 200S, Dual200S, 300S, 600S, OasisMist | |
| humidityHigh | Switch | Indicator for high humidity | 200S, Dual200S, 300S, 600S, OasisMist | |
| waterTankLifted | Switch | Indicator for whether the water tank is removed | 200S, Dual200S, 300S, 600S, OasisMist | |
| **stopAtHumiditySetpoint** | Switch | Whether the unit is set to stop when the set point is reached | 200S, Dual200S, 300S, 600S, OasisMist | [ON, OFF] |
| humidity | Number:Dimensionless | Indicator for the currently measured humidity % level | 200S, Dual200S, 300S, 600S, OasisMist | |
| **mistLevel** | Number:Dimensionless | The current mist level set | 300S | [1...2] |
| **mistLevel** | Number:Dimensionless | The current mist level set | 200S, Dual200S, 600S, OasisMist | [1...3] |
| **humidifierMode** | String | The current mode of operation | 200S, Dual200S, 300S, 600S, OasisMist | [auto, sleep] |
| **nightLightMode** | String | The night light mode | 200S, Dual200S, 300S | [on, dim, off] |
| **humiditySetpoint** | Number:Dimensionless | Humidity % set point to reach | 200S, Dual200S, 300S, 600S, OasisMist | [30...80] |
| warmEnabled | Switch | Indicator for warm mist mode | 600S, OasisMist | |
| Channel | Type | Description | Model's Supported | Controllable Values | 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, OasisMist (EU Model) | [auto, manual] | |
| **humidifierMode** | String | The current mode of operation | 300S, 600S, OasisMist1000, OasisMist (Non EU Models) | [auto, manual, sleep] | |
| **nightLightMode** | String | The night light mode | 200S, Dual200S, 300S, OasisMist (EU Model) | [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 +136,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 +144,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 +154,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 +162,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 +230,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 +246,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 +286,7 @@ Frame {
}
```
#### Air Purifier Core 200S/300S Model
#### Air Purifier Core 200S / 300S Model
```perl
Frame {
@ -251,6 +303,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 +375,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 +385,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 +393,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 +411,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]"
}
```

View File

@ -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";

View File

@ -18,11 +18,11 @@ import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.vesync.internal.api.IHttpClientProvider;
import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler;
import org.openhab.binding.vesync.internal.handlers.VeSyncDeviceAirHumidifierHandler;
import org.openhab.binding.vesync.internal.handlers.VeSyncDeviceAirPurifierHandler;
import org.openhab.core.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.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@ -41,12 +42,23 @@ import org.osgi.service.component.annotations.Reference;
*/
@NonNullByDefault
@Component(configurationPid = "binding.vesync", service = ThingHandlerFactory.class)
public class VeSyncHandlerFactory extends BaseThingHandlerFactory implements IHttpClientProvider {
public class VeSyncHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> 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 class VeSyncHandlerFactory extends BaseThingHandlerFactory implements IHt
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;
}
}

View File

@ -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();
}

View File

@ -34,6 +34,7 @@ import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncLoginCredentials;
@ -58,13 +59,16 @@ 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 final @Nullable HttpClient httpClient;
private Map<String, @NotNull VeSyncManagedDeviceBase> macLookup;
public VeSyncV2ApiHelper() {
public VeSyncV2ApiHelper(final HttpClient httpClient) {
this.httpClient = httpClient;
macLookup = new HashMap<>();
}
@ -72,13 +76,9 @@ public class VeSyncV2ApiHelper {
return macLookup;
}
/**
* Sets the httpClient object to be used for API calls to Vesync.
*
* @param httpClient the client to be used.
*/
public void setHttpClient(@Nullable HttpClient httpClient) {
this.httpClient = httpClient;
public void dispose() {
loggedInSession = null;
macLookup.clear();
}
public static @NotNull String calculateMd5(final @Nullable String password) {
@ -154,6 +154,7 @@ public class VeSyncV2ApiHelper {
}
veSyncRequestManagedDeviceBypassV2.cid = deviceData.cid;
veSyncRequestManagedDeviceBypassV2.configModule = deviceData.configModule;
veSyncRequestManagedDeviceBypassV2.configModel = deviceData.configModule;
veSyncRequestManagedDeviceBypassV2.deviceRegion = deviceData.deviceRegion;
}
return reqV1Authorized(url, requestData);
@ -167,16 +168,22 @@ public class VeSyncV2ApiHelper {
private String directReqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData)
throws AuthenticationException {
try {
Request request = httpClient.POST(url);
final HttpClient client = httpClient;
if (client == null) {
throw new AuthenticationException("No HTTP Client");
}
Request request = client.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 +227,12 @@ public class VeSyncV2ApiHelper {
private VeSyncLoginResponse processLogin(String username, String password, String timezone)
throws AuthenticationException {
try {
Request request = httpClient.POST(V1_LOGIN_ENDPOINT);
final HttpClient client = httpClient;
if (client == null) {
throw new AuthenticationException("No HTTP Client");
}
Request request = client.newRequest(V1_LOGIN_ENDPOINT).method(HttpMethod.POST).timeout(RESPONSE_TIMEOUT_SEC,
TimeUnit.SECONDS);
// No headers for login
request.content(new StringContentProvider(
@ -228,7 +240,7 @@ public class VeSyncV2ApiHelper {
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);

View File

@ -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";

View File

@ -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 @@ import com.google.gson.annotations.SerializedName;
*/
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;
}
}

View File

@ -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 VeSyncRequestManagedDeviceBypassV2 extends VeSyncAuthenticatedReque
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) {

View File

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

View File

@ -18,8 +18,8 @@ import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncRequestV1ManagedDeviceDetails} is the Java class as a DTO to hold login credentials for the Vesync
* API.
* 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
*/

View File

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

View File

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

View File

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

View File

@ -16,8 +16,7 @@ import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncV2BypassPurifierStatus} is a Java class used as a DTO to hold the Vesync's API's common response
* data,
* in regards to an Air Purifier device.
* data, in regards to an Air Purifier device.
*
* @author David Goodyear - Initial contribution
*/

View File

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

View File

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

View File

@ -53,6 +53,29 @@ public class VeSyncV1AirPurifierDeviceDetailsResponse extends VeSyncResponse {
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;

View File

@ -16,14 +16,9 @@ import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.V2_BYPASS_ENDPOINT;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.validation.constraints.NotNull;
@ -32,11 +27,14 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
import org.openhab.binding.vesync.internal.VeSyncDeviceConfiguration;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest;
import org.openhab.binding.vesync.internal.dto.requests.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 +45,10 @@ import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.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 +90,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 +168,7 @@ public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler {
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 +187,8 @@ public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler {
newProps = getMetadataProperities(metadata);
deviceId = metadata.getUuid();
// Refresh the device -> protocol mapping
deviceLookupKey = getValidatedIdString();
@ -404,13 +418,26 @@ public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler {
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 +449,7 @@ public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler {
}
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 +483,7 @@ public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler {
VeSyncRequestManagedDeviceBypassV2 readReq = new VeSyncRequestManagedDeviceBypassV2();
readReq.payload.method = method;
readReq.payload.data = payload;
readReq.deviceId = deviceId;
try {
if (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)) {
@ -526,17 +555,26 @@ public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler {
public static VeSyncDeviceMetadata getDeviceFamilyMetadata(final @Nullable String deviceType,
final String deviceProtocolPrefix, final List<VeSyncDeviceMetadata> metadata) {
if (deviceType == null) {
return UNKNOWN;
}
// First look for a direct ID match, if no matches are found scan for the matches based on the generation ID.
final Optional<VeSyncDeviceMetadata> directIdMatch = metadata.stream()
.filter(x -> x.nonStandardIds.contains(deviceType)).findFirst();
if (directIdMatch.isPresent()) {
return directIdMatch.get();
}
final String[] idParts = deviceType.split("-");
if (idParts.length == 3) {
if (!deviceProtocolPrefix.equals(idParts[0])) {
return UNKNOWN;
}
}
List<VeSyncDeviceMetadata> foundMatch = metadata.stream()
.filter(x -> x.deviceTypeIdMatches(deviceType, idParts)).collect(Collectors.toList());
final List<VeSyncDeviceMetadata> foundMatch = metadata.stream().filter(x -> x.deviceTypeIdMatches(idParts))
.toList();
if (foundMatch.size() == 1) {
return foundMatch.get(0);
} else {
@ -547,4 +585,15 @@ public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler {
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());
}
}
}

View File

@ -17,6 +17,7 @@ import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
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;
@ -27,7 +28,6 @@ import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
import org.openhab.binding.vesync.internal.api.IHttpClientProvider;
import org.openhab.binding.vesync.internal.api.VeSyncV2ApiHelper;
import org.openhab.binding.vesync.internal.discovery.DeviceMetaDataUpdatedHandler;
import org.openhab.binding.vesync.internal.discovery.VeSyncDiscoveryService;
@ -36,6 +36,9 @@ import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase
import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException;
import org.openhab.core.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 +49,9 @@ import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.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 +68,30 @@ 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 volatile int backgroundScanTime = -1;
protected final VeSyncV2ApiHelper api;
private final Logger logger = LoggerFactory.getLogger(VeSyncBridgeHandler.class);
private final Object scanConfigLock = new Object();
private final TranslationProvider translationProvider;
private final LocaleProvider localeProvider;
private final Bundle bundle;
private @Nullable ScheduledFuture<?> backgroundDiscoveryPollingJob;
protected final VeSyncV2ApiHelper api = new VeSyncV2ApiHelper();
private IHttpClientProvider httpClientProvider;
private volatile int backgroundScanTime = -1;
private final Object scanConfigLock = new Object();
public VeSyncBridgeHandler(Bridge bridge, @NotNull IHttpClientProvider httpClientProvider) {
public VeSyncBridgeHandler(Bridge bridge, @Reference HttpClientFactory httpClientFactory,
@Reference TranslationProvider translationProvider, @Reference LocaleProvider localeProvider) {
super(bridge);
this.httpClientProvider = httpClientProvider;
api = new VeSyncV2ApiHelper(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 +162,8 @@ public class VeSyncBridgeHandler extends BaseBridgeHandler implements VeSyncClie
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,8 +216,6 @@ public class VeSyncBridgeHandler extends BaseBridgeHandler implements VeSyncClie
@Override
public void initialize() {
api.setHttpClient(httpClientProvider.getHttpClient());
VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class);
scheduler.submit(() -> {
@ -211,7 +227,8 @@ public class VeSyncBridgeHandler extends BaseBridgeHandler implements VeSyncClie
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.
@ -222,12 +239,12 @@ public class VeSyncBridgeHandler extends BaseBridgeHandler implements VeSyncClie
@Override
public void dispose() {
setBackgroundScanInterval(DEFAULT_DEVICE_SCAN_DISABLED);
api.setHttpClient(null);
api.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) {

View File

@ -15,17 +15,28 @@ package org.openhab.binding.vesync.internal.handlers;
import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.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.ThingStatus;
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,38 +59,60 @@ import org.slf4j.LoggerFactory;
* @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";
public static final String DEV_FAMILY_DUAL_200S = "Dual 200S";
public static final String DEV_FAMILY_600S = "600S";
public static final String DEV_FAMILY_OASIS_MIST_EU = "Oasis Mist EU";
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<String> 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<String> 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<String> 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_EU = new VeSyncDeviceHumidifierMetadata(1,
DEV_FAMILY_OASIS_MIST_EU, Collections.emptyList(), Arrays.asList("LUH-O451S-WEU"), AUTO_MAN_MODES, 1, 3, 0,
3, false, CLASSIC_300S_NIGHT_LIGHT_MODES);
public static final VeSyncDeviceHumidifierMetadata OASIS_MIST = new VeSyncDeviceHumidifierMetadata(1,
DEV_FAMILY_OASIS_MIST, Arrays.asList("O451S", "O601S"), 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<VeSyncDeviceMetadata> SUPPORTED_MODEL_FAMILIES = Arrays.asList(LV600S, CLASSIC300S,
CLASSIC200S, DUAL200S, OASIS_MIST);
private static final List<String> CLASSIC_300S_600S_MODES = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP);
private static final List<String> CLASSIC_300S_NIGHT_LIGHT_MODES = Arrays.asList(MODE_ON, MODE_DIM, MODE_OFF);
CLASSIC200S, DUAL200S, OASIS_MIST, OASIS_MIST_EU);
private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirHumidifierHandler.class);
@ -86,8 +120,21 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
private final Object pollLock = new Object();
public VeSyncDeviceAirHumidifierHandler(Thing thing) {
super(thing);
public static final Map<String, VeSyncDeviceHumidifierMetadata> DEV_FAMILY_HUMIDIFER_MAP = new HashMap<String, VeSyncDeviceHumidifierMetadata>() {
{
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_EU.deviceFamilyName, OASIS_MIST_EU);
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 +144,25 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
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;
case DEV_FAMILY_OASIS_MIST_EU:
toRemove = new String[] { DEVICE_CHANNEL_AF_SCHEDULES_COUNT, DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME };
break;
}
}
@ -152,6 +209,11 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
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 +233,44 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
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 +293,42 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
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 +357,16 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
@Override
protected void pollForDeviceData(final ExpiringCache<String> 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 +382,11 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
return;
}
if (devContraints.protocolV2Version == 2) {
humidifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2Ver2BypassHumidifierStatus.class);
} else {
humidifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassHumidifierStatus.class);
}
if (humidifierStatus == null) {
return;
@ -325,12 +406,19 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
updateStatus(ThingStatus.ONLINE);
}
if (!"0".equals(humidifierStatus.result.getCode())) {
logger.warn("Check correct Thing type has been set - API gave a unexpected response for an Air Humidifier");
return;
if (devContraints.protocolV2Version != 2) {
parseV2Ver1Poll((VeSyncV2BypassHumidifierStatus) humidifierStatus, deviceFamily);
} else {
parseV2Ver2Poll((VeSyncV2Ver2BypassHumidifierStatus) humidifierStatus);
}
}
final String deviceFamily = getThing().getProperties().get(DEVICE_PROP_DEVICE_FAMILY);
private void parseV2Ver1Poll(final VeSyncV2BypassHumidifierStatus humidifierStatus,
final @Nullable String deviceFamily) {
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.enabled));
updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(humidifierStatus.result.result.display));
@ -342,6 +430,10 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
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 +446,43 @@ public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
} 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());
}
}
}

View File

@ -19,8 +19,11 @@ 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 java.util.stream.Collectors;
import javax.validation.constraints.NotNull;
@ -29,15 +32,23 @@ import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestV1ManagedDeviceDetails;
import org.openhab.binding.vesync.internal.dto.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;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
@ -45,6 +56,7 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -55,6 +67,7 @@ import org.slf4j.LoggerFactory;
* @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 +81,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<String> 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<String> FAN_MODES_NO_PET = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP);
private static final List<String> FAN_MODES_MAN_SLEEP = Arrays.asList(MODE_MANUAL, MODE_SLEEP);
private static final List<String> 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<String> 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<VeSyncDeviceMetadata> 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<String> CORE_400S600S_FAN_MODES = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP);
private static final List<String> CORE_200S300S_FAN_MODES = Arrays.asList(MODE_MANUAL, MODE_SLEEP);
private static final List<String> 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<String, VeSyncDevicePurifierMetadata> DEV_FAMILY_PURIFIER_MAP = new HashMap<String, VeSyncDevicePurifierMetadata>() {
{
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<VeSyncDeviceMetadata> SUPPORTED_MODEL_FAMILIES = DEV_FAMILY_PURIFIER_MAP.values().stream()
.collect(Collectors.toList());
private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirPurifierHandler.class);
@ -96,8 +136,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 +155,28 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
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 +217,152 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
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:
switch (deviceFamily) {
case DEV_FAMILY_VITAL_100S:
case DEV_FAMILY_VITAL_200S:
sendV2BypassControlCommand(DEVICE_SET_SWITCH,
new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload(command.equals(OnOffType.ON),
0));
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:
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:
switch (deviceFamily) {
case DEV_FAMILY_VITAL_100S:
case DEV_FAMILY_VITAL_200S:
sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK,
new VeSyncRequestManagedDeviceBypassV2.SetChildLock(command.equals(OnOffType.ON)));
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();
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;
}
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;
}
break;
}
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_VITAL_100S:
case DEV_FAMILY_VITAL_200S:
sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE,
new VeSyncRequestManagedDeviceBypassV2.SetWorkModePayload(targetFanMode));
break;
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));
}
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));
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;
}
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
int requestedLevel = quantityCommand.intValue();
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_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_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);
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;
}
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;
}
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;
}
break;
}
sendV2BypassControlCommand(DEVICE_SET_LEVEL,
new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_WIND,
requestedLevel));
new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0,
DEVICE_LEVEL_TYPE_WIND, requestedLevel));
}
break;
}
} else if (command instanceof RefreshType) {
@ -285,17 +379,10 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
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:
if (!DEV_FAMILY_PUR_131S.equals(deviceFamily)) {
processV2BypassPoll(cachedResponse);
break;
case DEV_FAMILY_PUR_131S:
} else {
processV1AirPurifierPoll(cachedResponse);
break;
}
}
@ -312,13 +399,13 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
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 +430,7 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
}
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 +440,22 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
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<String> 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 +470,11 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
if (response.equals(EMPTY_STRING)) {
return;
}
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 +494,16 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
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,15 +518,12 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
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));
updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE, new QuantityType<>(
purifierStatus.result.result.configuration.autoPreference.roomSize, ImperialUnits.SQUARE_FOOT));
// Only 400S appears to have this JSON extension object
if (purifierStatus.result.result.extension != null) {
@ -436,4 +542,28 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
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));
}
}

View File

@ -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<String> deviceGenerations, final List<String> nonStandardIds, final List<String> fanModes,
final int targetMinMistLevel, final int targetMaxMistLevel, final int targetMinWarmMistLevel,
final int targetMaxWarmMistLevel, final boolean remapsAutoToHumidity, List<String> 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<String> 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<String> nightLightModes;
}

View File

@ -50,14 +50,8 @@ public class VeSyncDeviceMetadata {
*/
public final List<String> 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;
public boolean deviceTypeIdMatches(final String[] deviceTypeSegments) {
return (deviceTypeSegments.length == 3 && deviceGenerations.contains(deviceTypeSegments[1]));
}
public String getDeviceFamilyName() {

View File

@ -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<String> deviceGenerations, final List<String> nonStandardIds, final List<String> fanModes,
final int minFanSpeed, final int maxFanSpeed, final List<String> 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<String> 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<String> 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);
}
}

View File

@ -45,10 +45,11 @@ 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
channel-type.vesync.deviceAFConfigAutoPrefRoomSizeType.description = Room size (foot sq) for efficient auto mode
channel-type.vesync.deviceAFConfigAutoPrefRoomSizeType.description = Room size for efficient auto mode
channel-type.vesync.deviceAFConfigAutoPrefType.label = Config: Auto Mode
channel-type.vesync.deviceAFConfigAutoPrefType.description = The operating mode when the air purifier is set to auto
channel-type.vesync.deviceAFConfigAutoPrefType.state.option.default = Auto (Air Quality)
@ -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

View File

@ -64,6 +64,8 @@
<channel id="configAutoRoomSize" typeId="deviceAFConfigAutoPrefRoomSizeType"/>
<channel id="schedulesCount" typeId="deviceAFConfigAutoScheduleCountType"/>
<channel id="nightLightMode" typeId="deviceAFNightLight"/>
<channel id="lightDetection" typeId="deviceAFLightDetection"/>
<channel id="lightDetected" typeId="deviceAFLightDetected"/>
</channels>
<properties>
@ -71,6 +73,7 @@
<property name="Device Type"/>
<property name="MAC Id"/>
<property name="Device Family"/>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>macId</representation-property>
@ -109,6 +112,8 @@
<channel id="humiditySetpoint" typeId="deviceConfigTargetHumidity"/>
<channel id="warmEnabled" typeId="warmModeEnabled"/>
<channel id="warmLevel" typeId="warmLevel"/>
<channel id="schedulesCount" typeId="deviceAFConfigAutoScheduleCountType"/>
<channel id="timerExpiry" typeId="deviceAFTimerExpiry"/>
</channels>
<properties>
@ -116,6 +121,7 @@
<property name="Device Type"/>
<property name="MAC Id"/>
<property name="Device Family"/>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>macId</representation-property>
@ -154,10 +160,10 @@
</channel-type>
<channel-type id="deviceFilterLifePercentageType">
<item-type>Number:Dimensionless</item-type>
<item-type unitHint="%">Number:Dimensionless</item-type>
<label>Filter Life Remaining</label>
<description>Indicator of the remaining filter life</description>
<state readOnly="true" pattern="%.0f %%"/>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="airPurifierModeType">
@ -169,6 +175,7 @@
<option value="auto">Auto</option>
<option value="manual">Manual Fan Control</option>
<option value="sleep">Sleeping Auto</option>
<option value="pet">Pet Auto</option>
</options>
</state>
</channel-type>
@ -188,14 +195,14 @@
<channel-type id="airPurifierFanLevelType">
<item-type>Number:Dimensionless</item-type>
<item-type unitHint="one">Number:Dimensionless</item-type>
<label>Fan Speed</label>
<description>Indicator of the current fan speed</description>
<state readOnly="true" pattern="%.0f"/>
</channel-type>
<channel-type id="deviceErrorCodeType">
<item-type>Number:Dimensionless</item-type>
<item-type unitHint="one">Number:Dimensionless</item-type>
<label>Device Error Code</label>
<description>Indicator of the current error code of the device</description>
<state readOnly="true" pattern="%.0f"/>
@ -209,7 +216,7 @@
</channel-type>
<channel-type id="airQualityPM25">
<item-type>Number:Density</item-type>
<item-type unitHint="µg/m³">Number:Density</item-type>
<label>Air Quality PPM2.5</label>
<description>Indicator of current air quality</description>
<state readOnly="true" pattern="%.0f %unit%"/>
@ -221,6 +228,20 @@
<description>Configuration: If the devices display is enabled forever</description>
</channel-type>
<channel-type id="deviceAFLightDetection">
<item-type>Switch</item-type>
<label>Light Detection</label>
<description>If the devices light detection is enabled</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="deviceAFLightDetected">
<item-type>Switch</item-type>
<label>Light Detected</label>
<description>Indicator if the device detects light</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="deviceAFConfigAutoPrefType">
<item-type>String</item-type>
<label>Config: Auto Mode</label>
@ -242,14 +263,14 @@
</channel-type>
<channel-type id="deviceAFConfigAutoPrefRoomSizeType">
<item-type>Number:Dimensionless</item-type>
<item-type unitHint="ft2">Number:Area</item-type>
<label>Config: Room size</label>
<description>Room size (foot sq) for efficient auto mode</description>
<state readOnly="true" pattern="%.0f sq ft"/>
<description>Room size for efficient auto mode</description>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="deviceAFConfigAutoScheduleCountType">
<item-type>Number:Dimensionless</item-type>
<item-type unitHint="one">Number:Dimensionless</item-type>
<label>Config: Schedules Count</label>
<description>The current number of schedules configured</description>
<state readOnly="true" pattern="%.0f"/>
@ -299,7 +320,7 @@
</channel-type>
<channel-type id="deviceMistLevelType">
<item-type>Number:Dimensionless</item-type>
<item-type unitHint="one">Number:Dimensionless</item-type>
<label>Mist Level</label>
<description>System representation of mist level</description>
<state readOnly="false" pattern="%.0f"/>
@ -326,11 +347,10 @@
</channel-type>
<channel-type id="warmLevel">
<item-type>Number:Dimensionless</item-type>
<item-type unitHint="one">Number:Dimensionless</item-type>
<label>Warm Level</label>
<description>Warm Level</description>
<state readOnly="false" pattern="%.0f"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="vesync:airHumidifier">
<instruction-set targetVersion="1">
<add-channel id="schedulesCount">
<type>vesync:deviceAFConfigAutoScheduleCountType</type>
</add-channel>
<add-channel id="timerExpiry">
<type>vesync:deviceAFTimerExpiry</type>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="vesync:airPurifier">
<instruction-set targetVersion="1">
<add-channel id="lightDetection">
<type>vesync:deviceAFLightDetection</type>
</add-channel>
<add-channel id="lightDetected">
<type>vesync:deviceAFLightDetected</type>
</add-channel>
<update-channel id="fanMode">
<type>vesync:airPurifierModeType</type>
</update-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>