[veSync] New VeSync binding addition (#12219)

* [veSync] New VeSync binding addition

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] New VeSync binding addition - LUH-D301S support added.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] New VeSync binding addition - AH channel corrections

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] New VeSync binding addition - AH D301S night light removal

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] New VeSync binding addition - AH docs mistLevel correction

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] New VeSync binding addition - Debug output correction

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] New VeSync binding addition - Dual200S adjustments

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - removal of TODOs

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - markdown table formatting

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - Air Purifier doc's and bug fix

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - Air Humidifiers doc's

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - HttpClient handling management to move api instance to the correct location

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - ThingTypeUID additions

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - Documentation correction - airPurifierPollinterval

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - Documentation correction - configuration parameters

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - Module documentation correction - description update.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - thing-types - bridge configuration updates

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - thing-types - description updates to cut length where possible.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - discovery - representation prop adjustments

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR adjustments - documentation - configuration block adjustments.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustments - Humidity set point channel renames

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustments - Initalize direct call cleanup

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustments - getDeviceUID override removal

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustments - unit adjustments

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PMD Error correction - file naming correction

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustment - HttpClient handling simplified.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustment - Removal of dead code.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustment - Readme OpenHab to openHAB

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustment - Comment cleanup

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustment - Handler Error removal to comm issue

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustment - Quick spotless fix

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Adjustment - Removal of debug log - as status has message in now.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PPM to PM correction for Air Quality units.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] ReadMe Units PM Update

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] Constant name correction for air quality units adjustment.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] Humidifier Percentage Units addition.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] Air Filter Life Remaining units addition

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PM25 update based on other bindings, to correct the units.

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] ReadMe PM25 updates

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR Updates: Thing Type Ids to lower case

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR: Removal of unrequired createThing override

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR: Removal of unused channel-type nightLightBrightnessType

Signed-off-by: David Goodyear <david.goodyear@gmail.com>

* [veSync] PR: Readme correction

Signed-off-by: David Goodyear <david.goodyear@gmail.com>
This commit is contained in:
dag81 2022-05-07 11:15:48 +01:00 committed by GitHub
parent 7323b2e4eb
commit 0a46724b38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 4987 additions and 0 deletions

View File

@ -1681,6 +1681,11 @@
<artifactId>org.openhab.binding.verisure</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.vesync</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.vigicrues</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,303 @@
# VeSync Binding
Its current support is for the Air Purifiers & Humidifer's branded as Levoit which utilise the VeSync app based on the V2 protocol.
### Verified Models
Air Filtering models supported are Core300S, Core400S.
Air Humidifier models supported are Dual 200S, Classic 300S, 600S.
### Awaiting User Verification Models
Air Filtering models supported are Core200S and Core600S.
Air Humidifier Classic 200S (Same as 300S without the nightlight from initial checks)
## Supported Things
This binding supports the follow thing types:
| Thing | Thing Type | Thing Type UID | Discovery | Description |
|----------------|------------|----------------|-----------|----------------------------------------------------------------------|
| Bridge | Bridge | bridge | Manual | A single connection to the VeSync API |
| Air Purifier | Thing | airPurifier | Automatic | A Air Purifier supporting V2 e.g. Core200S/Core300S or Core400S unit |
| Air Humidifier | Thing | airHumidifier | Automatic | A Air Humidifier supporting V2 e.g. Classic300S or 600s |
This binding was developed from the great work in the listed projects.
The only Air Filter unit it has been tested against is the Core400S unit, **I'm looking for others to confirm** my queries regarding **the Core200S and Core300S** units.
The ***Classic 300S Humidifier*** has been tested, and ***600S with current warm mode restrictions***.
## Discovery
Once the bridge is configured auto discovery will discover supported devices from the VeSync API.
## Thing Configuration
### Bridge configuration parameters
| Name | Type | Description | Recommended Values |
|----------------------------------|--------|-----------------------------------------------------------|--------------------|
| username | String | The username as used in the VeSync mobile application | |
| password | String | The password as used in the VeSync mobile application | |
| airPurifierPollInterval | Number | The poll interval (seconds) for air filters / humidifiers | 60 |
| backgroundDeviceDiscovery | Switch | Should the system scan periodically for new devices | ON |
| refreshBackgroundDeviceDiscovery | Number | Frequency (seconds) of scans for new new devices | 120 |
* Note Air PM Levels don't usually change quickly - 60s seems reasonable if openHAB is controlling it and your don't want near instant feedback of physical interactions with the devices.
### AirPurifier configuration parameters
It is recommended to use the device name, for locating devices. For this to work all the devices should have a unique
name in the VeSync mobile application.
The mac address from the VeSync mobile application may not align to the one the API
uses, therefore it's best left not configured or taken from auto-discovered information.
Device's will be found communicated with via the MAC Id first and if unsuccessful then by the deviceName.
| Name | Type | Description |
|------------------------|-------------------------|---------------------------------------------------------------------|
| deviceName | String | The name given to the device under Settings -> Device Name |
| macId | String | The mac for the device under Settings -> Device Info -> MAC Address |
## Channels
Channel names in **bold** are read/write, everything else is read-only
### AirPurifier Thing
| Channel | Type | Description | Model's Supported | Controllable Values |
|----------------------|----------------------|------------------------------------------------------------|-------------------|-----------------------|
| **enabled** | Switch | Whether the hardware device is enabled (Switched on) | 600S, 400S, 300S | [ON, OFF] |
| **childLock** | Switch | Whether the child lock (display lock is enabled) | 600S, 400S, 300S | [ON, OFF] |
| **display** | Switch | Whether the display is enabled (display is shown) | 600S, 400S, 300S | [ON, OFF] |
| **fanMode** | String | The operation mode of the fan | 600S, 400S | [auto, manual, sleep] |
| **fanMode** | String | The operation mode of the fan | 200S, 300S, | [manual, sleep] |
| **manualFanSpeed** | Number:Dimensionless | The speed of the fan when in manual mode | 600S, 400S | [1...4] |
| **manualFanSpeed** | Number:Dimensionless | The speed of the fan when in manual mode | 300S | [1...3] |
| **nightLightMode** | String | The night lights mode | 200S, 300S | [on, dim, off] |
| filterLifePercentage | Number:Dimensionless | The remaining filter life as a percentage | 600S, 400S, 300S | |
| airQuality | Number:Dimensionless | The air quality as represented by the Core200S / Core300S | 600S, 400S, 300S | |
| airQualityPM25 | Number:Density | The air quality as represented by the Core400S | 600S, 400S, 300S | |
| errorCode | Number:Dimensionless | The error code reported by the device | 600S, 400S, 300S | |
| timerExpiry | DateTime | The expected expiry time of the current timer | 600S, 400S | |
| schedulesCount | Number:Dimensionless | The number schedules configured | 600S, 400S | |
| configDisplayForever | Switch | Config: Whether the display will disable when not active | 600S, 400S, 300S | |
| configAutoMode | String | Config: The mode of operation when auto is active | 600S, 400S, 300S | |
| configAutoRoomSize | Number:Dimensionless | Config: The room size set when auto utilises the room size | 600S, 400S, 300S | |
### AirHumidifier Thing
| Channel | Type | Description | Model's Supported | Controllable Values |
|----------------------------|----------------------|---------------------------------------------------------------|----------------------------|---------------------|
| **enabled** | Switch | Whether the hardware device is enabled (Switched on) | 200S, Dual200S, 300S, 600S | [ON, OFF] |
| **display** | Switch | Whether the display is enabled (display is shown) | 200S, Dual200S, 300S, 600S | [ON, OFF] |
| waterLacking | Switch | Indicator whether the unit is lacking water | 200S, Dual200S, 300S, 600S | |
| humidityHigh | Switch | Indicator for high humidity | 200S, Dual200S, 300S, 600S | |
| waterTankLifted | Switch | Indicator for whether the water tank is removed | 200S, Dual200S, 300S, 600S | |
| **stopAtHumiditySetpoint** | Switch | Whether the unit is set to stop when the set point is reached | 200S, Dual200S, 300S, 600S | [ON, OFF] |
| humidity | Number:Dimensionless | Indicator for the currently measured humidity % level | 200S, Dual200S, 300S, 600S | |
| **mistLevel** | Number:Dimensionless | The current mist level set | 300S | [1...2] |
| **mistLevel** | Number:Dimensionless | The current mist level set | 200S, Dual200S, 600S | [1...3] |
| **humidifierMode** | String | The current mode of operation | 200S, Dual200S, 300S, 600S | [auto, sleep] |
| **nightLightMode** | String | The night light mode | 200S, Dual200S, 300S | [on, dim, off] |
| **humiditySetpoint** | Number:Dimensionless | Humidity % set point to reach | 200S, Dual200S, 300S, 600S | [30...80] |
| warmEnabled | Switch | Indicator for warm mist mode | 600S | |
## Full Example
### Configuration (*.things)
#### Air Purifiers Core 200S/300S/400S Models & Air Humidifier Classic300S/600S Models
```
Bridge vesync:bridge:vesyncServers [username="<USERNAME>", password="<PASSWORD>", airPurifierPollInterval=60] {
airPurifier loungeAirFilter [deviceName="<DEVICE NAME FROM APP>"]
airPurifier bedroomAirFilter [deviceName="<DEVICE NAME FROM APP>"]
airHumidifier loungeHumidifier [deviceName="<DEVICE NAME FROM APP>"]
}
```
### Configuration (*.items)
#### Air Purifier Core 400S / 600S Model
```
Switch LoungeAPPower "Lounge Air Purifier Power" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:enabled" }
Switch LoungeAPDisplay "Lounge Air Purifier Display" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:display" }
Switch LoungeAPControlsLock "Lounge Air Purifier Controls Locked" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:childLock" }
Number:Dimensionless LoungeAPFilterRemainingUse "Lounge Air Purifier Filter Remaining [%.0f %unit%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:filterLifePercentage" }
String LoungeAPMode "Lounge Air Purifier Mode [%s]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:fanMode" }
Number:Dimensionless LoungeAPManualFanSpeed "Lounge Air Purifier Manual Fan Speed" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:manualFanSpeed" }
Number:Density LoungeAPAirQuality "Lounge Air Purifier Air Quality [%.0f% %unit%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:airQualityPM25" }
Number LoungeAPErrorCode "Lounge Air Purifier Error Code" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:errorCode" }
String LoungeAPAutoMode "Lounge Air Purifier Auto Mode" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:configAutoMode" }
Number LoungeAPAutoRoomSize "Lounge Air Purifier Auto Room Size [%.0f% sqft]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:configAutoRoomSize" }
Number:Time LoungeAPTimerLeft "Lounge Air Purifier Timer Left [%1$Tp]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:timerRemain" }
DateTime LoungeAPTimerExpiry "Lounge Air Purifier Timer Expiry [%1$tA %1$tI:%1$tM %1$Tp]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:timerExpiry" }
Number LoungeAPSchedulesCount "Lounge Air Purifier Schedules Count" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:schedulesCount" }
```
#### Air Purifier Core 200S/300S Model
```
Switch LoungeAPPower "Lounge Air Purifier Power" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:enabled" }
Switch LoungeAPDisplay "Lounge Air Purifier Display" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:display" }
String LoungeAPNightLightMode "Lounge Air Purifier Night Light Mode" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:nightLightMode" }
Switch LoungeAPControlsLock "Lounge Air Purifier Controls Locked" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:childLock" }
Number:Dimensionless LoungeAPFilterRemainingUse "Lounge Air Purifier Filter Remaining [%.0f %unit%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:filterLifePercentage" }
String LoungeAPMode "Lounge Air Purifier Mode [%s]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:fanMode" }
Number:Dimensionless LoungeAPManualFanSpeed "Lounge Air Purifier Manual Fan Speed" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:manualFanSpeed" }
Number:Density LoungeAPAirQuality "Lounge Air Purifier Air Quality [%.0f%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:airQuality" }
Number LoungeAPErrorCode "Lounge Air Purifier Error Code" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:errorCode" }
String LoungeAPAutoMode "Lounge Air Purifier Auto Mode" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:configAutoMode" }
Number LoungeAPAutoRoomSize "Lounge Air Purifier Auto Room Size [%.0f% sqft]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:configAutoRoomSize" }
Number:Time LoungeAPTimerLeft "Lounge Air Purifier Timer Left [%1$Tp]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:timerRemain" }
DateTime LoungeAPTimerExpiry "Lounge Air Purifier Timer Expiry [%1$tA %1$tI:%1$tM %1$Tp]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:timerExpiry" }
Number LoungeAPSchedulesCount "Lounge Air Purifier Schedules Count" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:schedulesCount" }
```
#### Air Humidifier Classic 200S / Dual 200S Model
```
Switch LoungeAHPower "Lounge Air Humidifier Power" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:enabled" }
Switch LoungeAHDisplay "Lounge Air Humidifier Display" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:display" }
String LoungeAHMode "Lounge Air Humidifier Mode" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidifierMode" }
Switch LoungeAHWaterLacking "Lounge Air Humidifier Water Lacking" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterLacking" }
Switch LoungeAHHighHumidity "Lounge Air Humidifier High Humidity" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidityHigh" }
Switch LoungeAHWaterTankRemoved "Lounge Air Humidifier Water Tank Removed" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterTankLifted" }
Number:Dimensionless LoungeAHHumidity "Lounge Air Humidifier Measured Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidity" }
Switch LoungeAHTargetStop "Lounge Air Humidifier Stop at target" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:stopAtTargetLevel" }
Number:Dimensionless LoungeAHTarget "Lounge Air Humidifier Target Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humiditySetpoint" }
Number:Dimensionless LoungeAHMistLevel "Lounge Air Humidifier Mist Level" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:mistLevel" }
```
#### Air Humidifier Classic 300S Model
```
Switch LoungeAHPower "Lounge Air Humidifier Power" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:enabled" }
Switch LoungeAHDisplay "Lounge Air Humidifier Display" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:display" }
String LoungeAHNightLightMode "Lounge Air Humidifier Night Light Mode" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:nightLightMode }
String LoungeAHMode "Lounge Air Humidifier Mode" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidifierMode" }
Switch LoungeAHWaterLacking "Lounge Air Humidifier Water Lacking" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterLacking" }
Switch LoungeAHHighHumidity "Lounge Air Humidifier High Humidity" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidityHigh" }
Switch LoungeAHWaterTankRemoved "Lounge Air Humidifier Water Tank Removed" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterTankLifted" }
Number:Dimensionless LoungeAHHumidity "Lounge Air Humidifier Measured Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidity" }
Switch LoungeAHTargetStop "Lounge Air Humidifier Stop at target" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:stopAtTargetLevel" }
Number:Dimensionless LoungeAHTarget "Lounge Air Humidifier Target Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humiditySetpoint" }
Number:Dimensionless LoungeAHMistLevel "Lounge Air Humidifier Mist Level" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:mistLevel" }
```
#### Air Humidifier 600S Model
```
Switch LoungeAHPower "Lounge Air Humidifier Power" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:enabled" }
Switch LoungeAHDisplay "Lounge Air Humidifier Display" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:display" }
String LoungeAHMode "Lounge Air Humidifier Mode" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidifierMode" }
Switch LoungeAHWaterLacking "Lounge Air Humidifier Water Lacking" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterLacking" }
Switch LoungeAHHighHumidity "Lounge Air Humidifier High Humidity" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidityHigh" }
Switch LoungeAHWaterTankRemoved "Lounge Air Humidifier Water Tank Removed" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:waterTankLifted" }
Number:Dimensionless LoungeAHHumidity "Lounge Air Humidifier Measured Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humidity" }
Switch LoungeAHTargetStop "Lounge Air Humidifier Stop at target" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:stopAtTargetLevel" }
Number:Dimensionless LoungeAHTarget "Lounge Air Humidifier Target Humidity [%.0f %unit%]" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:humiditySetpoint" }
Number:Dimensionless LoungeAHMistLevel "Lounge Air Humidifier Mist Level" { channel="vesync:airHumidifier:vesyncServers:loungeHumidifier:mistLevel" }
```
### Configuration (*.sitemap)
#### Air Purifier Core 400S / 600S Model
```
Frame {
Switch item=LoungeAPPower label="Power"
Text item=LoungeAPFilterRemainingUse label="Filter Remaining"
Switch item=LoungeAPDisplay label="Display"
Text item=LoungeAPAirQuality label="Air Quality [%.0f (PM2.5)]"
Switch item=LoungeAPControlsLock label="Controls Locked"
Text item=LoungeAPTimerExpiry label="Timer Shutdown @" icon="clock"
Switch item=LoungeAPMode label="Mode" mappings=[auto="Auto", manual="Manual Fan Control", sleep="Sleeping"] icon="settings"
Text item=LoungeAPErrorCode label="Error Code [%.0f]"
Switch item=LoungeAPManualFanSpeed label="Manual Fan Speed [%.0f]" mappings=[1="1", 2="2", 3="3", 4="4"] icon="settings"
}
```
#### Air Purifier Core 200S/300S Model
```
Frame {
Switch item=LoungeAPPower label="Power"
Text item=LoungeAPFilterRemainingUse label="Filter Remaining"
Switch item=LoungeAPDisplay label="Display"
Switch item=LoungeAPNightLightMode label="Night Light Mode" mappings=[on="On", dim="Dimmed", off="Off"] icon="settings"
Text item=LoungeAPAirQuality label="Air Quality [%.0f]"
Switch item=LoungeAPControlsLock label="Controls Locked"
Text item=LoungeAPTimerExpiry label="Timer Shutdown @" icon="clock"
Switch item=LoungeAPMode label="Mode" mappings=[manual="Manual Fan Control", sleep="Sleeping"] icon="settings"
Text item=LoungeAPErrorCode label="Error Code [%.0f]"
Switch item=LoungeAPManualFanSpeed label="Manual Fan Speed [%.0f]" mappings=[1="1", 2="2", 3="3"] icon="settings"
}
```
#### Air Humidifier Classic 200S / Dual 200S Model
```
Frame {
Switch item=LoungeAHPower
Switch item=LoungeAHDisplay
Switch item=LoungeAHMode label="Mode" mappings=[auto="Auto", sleep="Sleeping"] icon="settings"
Text icon="none" item=LoungeAHWaterLacking
Text icon="none" item=LoungeAHHighHumidity
Text icon="none" item=LoungeAHWaterTankRemoved
Text icon="none" item=LoungeAHHumidity
Switch item=LoungeAHTargetStop
Slider item=LoungeAHTarget minValue=30 maxValue=80
Slider item=LoungeAHMistLevel minValue=1 maxValue=3
}
```
#### Air Humidifier Classic 300S Model
```
Frame {
Switch item=LoungeAHPower
Switch item=LoungeAHDisplay
Switch item=LoungeAHNightLightMode label="Night Light Mode" mappings=[on="On", dim="Dimmed", off="Off"] icon="settings"
Switch item=LoungeAHMode label="Mode" mappings=[auto="Auto", sleep="Sleeping"] icon="settings"
Text icon="none" item=LoungeAHWaterLacking
Text icon="none" item=LoungeAHHighHumidity
Text icon="none" item=LoungeAHWaterTankRemoved
Text icon="none" item=LoungeAHHumidity
Switch item=LoungeAHTargetStop
Slider item=LoungeAHTarget minValue=30 maxValue=80
Slider item=LoungeAHMistLevel minValue=1 maxValue=3
}
```
#### Air Humidifier 600S Model
```
Frame {
Switch item=LoungeAHPower
Switch item=LoungeAHDisplay
Switch item=LoungeAHMode label="Mode" mappings=[auto="Auto", sleep="Sleeping"] icon="settings"
Text icon="none" item=LoungeAHWaterLacking
Text icon="none" item=LoungeAHHighHumidity
Text icon="none" item=LoungeAHWaterTankRemoved
Text icon="none" item=LoungeAHHumidity
Switch item=LoungeAHTargetStop
Slider item=LoungeAHTarget minValue=30 maxValue=80
Slider item=LoungeAHMistLevel minValue=1 maxValue=3
}
```
### Credits
The binding code is based on a lot of work done by other developers:
- Contributors of (https://github.com/webdjoe/pyvesync) - Python interface for VeSync
- Rene Scherer, Holger Eisold - (https://www.openhab.org/addons/bindings/surepetcare) Sure Petcare Binding for openHAB as a reference point for the starting blocks of this code

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.vesync</artifactId>
<name>openHAB Add-ons :: Bundles :: VeSync Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.vesync-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-vesync" description="VeSync Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.vesync/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link VeSyncBridgeConfiguration} class contains fields mapping the configuration parameters for the bridge.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VeSyncBridgeConfiguration {
/**
* The clear text password to access the vesync API.
*/
@Nullable
public String password = "";
/**
* The email address / username to access the vesync API.
*/
@Nullable
public String username = "";
/**
* The polling interval to use for air purifier devices.
*/
@Nullable
public Integer airPurifierPollInterval;
}

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The {@link VeSyncConstants} class defines common constants, which are
* used across the whole binding.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VeSyncConstants {
public static final Gson GSON = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting()
.disableHtmlEscaping().serializeNulls().create();
private static final String BINDING_ID = "vesync";
public static final long DEFAULT_REFRESH_INTERVAL_DISCOVERED_DEVICES = 3600;
public static final long DEFAULT_POLL_INTERVAL_AIR_FILTERS_DEVICES = 10;
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
public static final ThingTypeUID THING_TYPE_AIR_PURIFIER = new ThingTypeUID(BINDING_ID, "airPurifier");
public static final ThingTypeUID THING_TYPE_AIR_HUMIDIFIER = new ThingTypeUID(BINDING_ID, "airHumidifier");
// Thing configuration properties
public static final String DEVICE_MAC_ID = "macAddress";
public static final String EMPTY_STRING = "";
// Base Device Channel Names
public static final String DEVICE_CHANNEL_ENABLED = "enabled";
public static final String DEVICE_CHANNEL_DISPLAY_ENABLED = "display";
public static final String DEVICE_CHANNEL_CHILD_LOCK_ENABLED = "childLock";
public static final String DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING = "filterLifePercentage";
public static final String DEVICE_CHANNEL_FAN_MODE_ENABLED = "fanMode";
public static final String DEVICE_CHANNEL_FAN_SPEED_ENABLED = "manualFanSpeed";
public static final String DEVICE_CHANNEL_ERROR_CODE = "errorCode";
public static final String DEVICE_CHANNEL_AIRQUALITY_BASIC = "airQuality";
public static final String DEVICE_CHANNEL_AIRQUALITY_PM25 = "airQualityPM25";
public static final String DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER = "configDisplayForever";
public static final String DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF = "configAutoMode";
public static final String DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME = "timerExpiry";
public static final String DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE = "configAutoRoomSize";
public static final String DEVICE_CHANNEL_AF_SCHEDULES_COUNT = "schedulesCount";
public static final String DEVICE_CHANNEL_AF_NIGHT_LIGHT = "nightLightMode";
// Humidity related channels
public static final String DEVICE_CHANNEL_WATER_LACKS = "waterLacking";
public static final String DEVICE_CHANNEL_HUMIDITY_HIGH = "humidityHigh";
public static final String DEVICE_CHANNEL_WATER_TANK_LIFTED = "waterTankLifted";
public static final String DEVICE_CHANNEL_STOP_AT_TARGET = "stopAtHumiditySetpoint";
public static final String DEVICE_CHANNEL_HUMIDITY = "humidity";
public static final String DEVICE_CHANNEL_MIST_LEVEL = "mistLevel";
public static final String DEVICE_CHANNEL_HUMIDIFIER_MODE = "humidifierMode";
public static final String DEVICE_CHANNEL_WARM_ENABLED = "warmEnabled";
public static final String DEVICE_CHANNEL_WARM_LEVEL = "warmLevel";
public static final String DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY = "humiditySetpoint";
// Property name constants
public static final String DEVICE_PROP_DEVICE_NAME = "Device Name";
public static final String DEVICE_PROP_DEVICE_TYPE = "Device Type";
public static final String DEVICE_PROP_DEVICE_MAC_ID = "MAC Id";
public static final String DEVICE_PROP_DEVICE_UUID = "UUID";
// Property name for config constants
public static final String DEVICE_PROP_CONFIG_DEVICE_NAME = "deviceName";
public static final String DEVICE_PROP_CONFIG_DEVICE_MAC = "macId";
// Bridge name constants
public static final String DEVICE_PROP_BRIDGE_REG_TS = "Registration Time";
public static final String DEVICE_PROP_BRIDGE_COUNTRY_CODE = "Country Code";
public static final String DEVICE_PROP_BRIDGE_ACCEPT_LANG = "Accept Language";
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link VeSyncDeviceConfiguration} class contains fields mapping the configuration parameters for a VeSync
* device's configuration.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VeSyncDeviceConfiguration {
/**
* The clear text device name as reported by the API.
*/
@Nullable
public String deviceName;
/**
* The mac address of the device as reported by the API.
*/
@Nullable
public String macId;
}

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal;
import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.vesync.internal.api.IHttpClientProvider;
import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler;
import org.openhab.binding.vesync.internal.handlers.VeSyncDeviceAirHumidifierHandler;
import org.openhab.binding.vesync.internal.handlers.VeSyncDeviceAirPurifierHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link org.openhab.binding.vesync.internal.VeSyncHandlerFactory} is responsible for creating
* things and thing handlers.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.vesync", service = ThingHandlerFactory.class)
public class VeSyncHandlerFactory extends BaseThingHandlerFactory implements IHttpClientProvider {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE,
THING_TYPE_AIR_PURIFIER, THING_TYPE_AIR_HUMIDIFIER);
private @Nullable HttpClient httpClientRef = null;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (VeSyncDeviceAirPurifierHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
return new VeSyncDeviceAirPurifierHandler(thing);
} else if (VeSyncDeviceAirHumidifierHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
return new VeSyncDeviceAirHumidifierHandler(thing);
} else if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
return new VeSyncBridgeHandler((Bridge) thing, this);
}
return null;
}
@Reference
protected void setHttpClientFactory(HttpClientFactory httpClientFactory) {
httpClientRef = httpClientFactory.getCommonHttpClient();
}
@Override
public @Nullable HttpClient getHttpClient() {
return httpClientRef;
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
/**
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public interface IHttpClientProvider {
@Nullable
HttpClient getHttpClient();
}

View File

@ -0,0 +1,254 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.api;
import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncLoginCredentials;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDevicesPage;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncLoginResponse;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDevicesPage;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncResponse;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException;
import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VeSyncV2ApiHelper {
private final Logger logger = LoggerFactory.getLogger(VeSyncV2ApiHelper.class);
private @NonNullByDefault({}) HttpClient httpClient;
private volatile @Nullable VeSyncUserSession loggedInSession;
private Map<String, @NotNull VeSyncManagedDeviceBase> macLookup;
public VeSyncV2ApiHelper() {
macLookup = new HashMap<>();
}
public Map<String, @NotNull VeSyncManagedDeviceBase> getMacLookupMap() {
return macLookup;
}
/**
* Sets the httpClient object to be used for API calls to Vesync.
*
* @param httpClient the client to be used.
*/
public void setHttpClient(@Nullable HttpClient httpClient) {
this.httpClient = httpClient;
}
public static @NotNull String calculateMd5(final @Nullable String password) {
if (password == null) {
return "";
}
MessageDigest md5;
StringBuilder md5Result = new StringBuilder();
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
return "";
}
byte[] handshakeHash = md5.digest(password.getBytes(StandardCharsets.UTF_8));
for (byte handshakeByte : handshakeHash) {
md5Result.append(String.format("%02x", handshakeByte));
}
return md5Result.toString();
}
public void discoverDevices() throws AuthenticationException {
try {
VeSyncRequestManagedDevicesPage reqDevPage = new VeSyncRequestManagedDevicesPage(loggedInSession);
boolean finished = false;
int pageNo = 1;
HashMap<String, VeSyncManagedDeviceBase> generatedMacLookup = new HashMap<>();
while (!finished) {
reqDevPage.pageNo = String.valueOf(pageNo);
reqDevPage.pageSize = String.valueOf(100);
final String result = reqV1Authorized(V1_MANAGED_DEVICES_ENDPOINT, reqDevPage);
VeSyncManagedDevicesPage resultsPage = VeSyncConstants.GSON.fromJson(result,
VeSyncManagedDevicesPage.class);
if (resultsPage == null || !resultsPage.outcome.getTotal().equals(resultsPage.outcome.getPageSize())) {
finished = true;
} else {
++pageNo;
}
if (resultsPage != null) {
for (VeSyncManagedDeviceBase device : resultsPage.outcome.list) {
logger.debug(
"Found device : {}, type: {}, deviceType: {}, connectionState: {}, deviceStatus: {}, deviceRegion: {}, cid: {}, configModule: {}, macID: {}, uuid: {}",
device.getDeviceName(), device.getType(), device.getDeviceType(),
device.getConnectionStatus(), device.getDeviceStatus(), device.getDeviceRegion(),
device.getCid(), device.getConfigModule(), device.getMacId(), device.getUuid());
// Update the mac address -> device table
generatedMacLookup.put(device.getMacId(), device);
}
}
}
macLookup = Collections.unmodifiableMap(generatedMacLookup);
} catch (final AuthenticationException ae) {
logger.warn("Failed background device scan : {}", ae.getMessage());
throw ae;
}
}
public String reqV2Authorized(final String url, final String macId, final VeSyncAuthenticatedRequest requestData)
throws AuthenticationException, DeviceUnknownException {
if (loggedInSession == null) {
throw new AuthenticationException("User is not logged in");
}
// Apply current session authentication data
requestData.applyAuthentication(loggedInSession);
// Apply specific addressing parameters
if (requestData instanceof VeSyncRequestManagedDeviceBypassV2) {
final VeSyncManagedDeviceBase deviceData = macLookup.get(macId);
if (deviceData == null) {
throw new DeviceUnknownException(String.format("Device not discovered with mac id: %s", macId));
}
((VeSyncRequestManagedDeviceBypassV2) requestData).cid = deviceData.cid;
((VeSyncRequestManagedDeviceBypassV2) requestData).configModule = deviceData.configModule;
((VeSyncRequestManagedDeviceBypassV2) requestData).deviceRegion = deviceData.deviceRegion;
}
return reqV1Authorized(url, requestData);
}
public String reqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData)
throws AuthenticationException {
try {
return directReqV1Authorized(url, requestData);
} catch (final AuthenticationException ae) {
throw ae;
}
}
private String directReqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData)
throws AuthenticationException {
try {
Request request = httpClient.POST(url);
// No headers for login
request.content(new StringContentProvider(VeSyncConstants.GSON.toJson(requestData)));
logger.debug("POST @ {} with content\r\n{}", url, VeSyncConstants.GSON.toJson(requestData));
request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8");
ContentResponse response = request.timeout(5, TimeUnit.SECONDS).send();
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
VeSyncResponse commResponse = VeSyncConstants.GSON.fromJson(response.getContentAsString(),
VeSyncResponse.class);
if (commResponse != null && (commResponse.isMsgSuccess() || commResponse.isMsgDeviceOffline())) {
logger.debug("Got OK response {}", response.getContentAsString());
return response.getContentAsString();
} else {
logger.debug("Got FAILED response {}", response.getContentAsString());
throw new AuthenticationException("Invalid JSON response from login");
}
} else {
logger.debug("HTTP Response Code: {}", response.getStatus());
logger.debug("HTTP Response Msg: {}", response.getReason());
throw new AuthenticationException(
"HTTP response " + response.getStatus() + " - " + response.getReason());
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new AuthenticationException(e);
}
}
public synchronized void login(final @Nullable String username, final @Nullable String password,
final @Nullable String timezone) throws AuthenticationException {
if (username == null || password == null || timezone == null) {
loggedInSession = null;
return;
}
try {
loggedInSession = processLogin(username, password, timezone).getUserSession();
} catch (final AuthenticationException ae) {
loggedInSession = null;
throw ae;
}
}
public void updateBridgeData(final VeSyncBridgeHandler bridge) {
bridge.handleNewUserSession(loggedInSession);
}
private VeSyncLoginResponse processLogin(String username, String password, String timezone)
throws AuthenticationException {
try {
Request request = httpClient.POST(V1_LOGIN_ENDPOINT);
// No headers for login
request.content(new StringContentProvider(
VeSyncConstants.GSON.toJson(new VeSyncLoginCredentials(username, password))));
request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8");
ContentResponse response = request.timeout(5, TimeUnit.SECONDS).send();
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
VeSyncLoginResponse loginResponse = VeSyncConstants.GSON.fromJson(response.getContentAsString(),
VeSyncLoginResponse.class);
if (loginResponse != null && loginResponse.isMsgSuccess()) {
logger.debug("Login successful");
return loginResponse;
} else {
throw new AuthenticationException("Invalid / unexpected JSON response from login");
}
} else {
logger.warn("Login Failed - HTTP Response Code: {} - {}", response.getStatus(), response.getReason());
throw new AuthenticationException(
"HTTP response " + response.getStatus() + " - " + response.getReason());
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new AuthenticationException(e);
}
}
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.discovery;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler;
/**
* The {@link DeviceMetaDataUpdatedHandler} enables call-backs for when the device meta-data is updated from a bridge.
* (VeSync Server Account).
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public interface DeviceMetaDataUpdatedHandler {
void handleMetadataRetrieved(VeSyncBridgeHandler handler);
}

View File

@ -0,0 +1,143 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.discovery;
import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.osgi.service.component.annotations.Component;
/**
* The {@link VeSyncDiscoveryService} is an implementation of a discovery service for VeSync devices. The meta-data is
* read by the bridge, and the discovery data updated via a callback implemented by the DeviceMetaDataUpdatedHandler.
*
* @author David Godyear - Initial contribution
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.vesync")
public class VeSyncDiscoveryService extends AbstractDiscoveryService
implements DiscoveryService, ThingHandlerService, DeviceMetaDataUpdatedHandler {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
private static final int DISCOVER_TIMEOUT_SECONDS = 5;
private @NonNullByDefault({}) VeSyncBridgeHandler bridgeHandler;
private @NonNullByDefault({}) ThingUID bridgeUID;
/**
* Creates a VeSyncDiscoveryService with enabled autostart.
*/
public VeSyncDiscoveryService() {
super(SUPPORTED_THING_TYPES, DISCOVER_TIMEOUT_SECONDS);
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_THING_TYPES;
}
@Override
public void activate() {
final Map<String, Object> properties = new HashMap<>();
properties.put(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, Boolean.TRUE);
super.activate(properties);
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof VeSyncBridgeHandler) {
bridgeHandler = (VeSyncBridgeHandler) handler;
bridgeUID = bridgeHandler.getUID();
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
@Override
protected void startBackgroundDiscovery() {
if (bridgeHandler != null) {
bridgeHandler.registerMetaDataUpdatedHandler(this);
}
}
@Override
protected void stopBackgroundDiscovery() {
if (bridgeHandler != null) {
bridgeHandler.unregisterMetaDataUpdatedHandler(this);
}
}
@Override
protected void startScan() {
// If the bridge is not online no other thing devices can be found, so no reason to scan at this moment.
removeOlderResults(getTimestampOfLastScan());
if (ThingStatus.ONLINE.equals(bridgeHandler.getThing().getStatus())) {
bridgeHandler.runDeviceScanSequenceNoAuthErrors();
}
}
@Override
public void handleMetadataRetrieved(VeSyncBridgeHandler handler) {
bridgeHandler.getAirPurifiersMetadata().map(apMeta -> {
final Map<String, Object> properties = new HashMap<>(6);
final String deviceUUID = apMeta.getUuid();
properties.put(DEVICE_PROP_DEVICE_NAME, apMeta.getDeviceName());
properties.put(DEVICE_PROP_DEVICE_TYPE, apMeta.getDeviceType());
properties.put(DEVICE_PROP_DEVICE_MAC_ID, apMeta.getMacId());
properties.put(DEVICE_PROP_DEVICE_UUID, deviceUUID);
properties.put(DEVICE_PROP_CONFIG_DEVICE_MAC, apMeta.getMacId());
properties.put(DEVICE_PROP_CONFIG_DEVICE_NAME, apMeta.getDeviceName());
return DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_AIR_PURIFIER, bridgeUID, deviceUUID))
.withLabel(apMeta.getDeviceName()).withBridge(bridgeUID).withProperties(properties)
.withRepresentationProperty(DEVICE_PROP_DEVICE_MAC_ID).build();
}).forEach(this::thingDiscovered);
bridgeHandler.getAirHumidifiersMetadata().map(apMeta -> {
final Map<String, Object> properties = new HashMap<>(6);
final String deviceUUID = apMeta.getUuid();
properties.put(DEVICE_PROP_DEVICE_NAME, apMeta.getDeviceName());
properties.put(DEVICE_PROP_DEVICE_TYPE, apMeta.getDeviceType());
properties.put(DEVICE_PROP_DEVICE_MAC_ID, apMeta.getMacId());
properties.put(DEVICE_PROP_DEVICE_UUID, deviceUUID);
properties.put(DEVICE_PROP_CONFIG_DEVICE_MAC, apMeta.getMacId());
properties.put(DEVICE_PROP_CONFIG_DEVICE_NAME, apMeta.getDeviceName());
return DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_AIR_HUMIDIFIER, bridgeUID, deviceUUID))
.withLabel(apMeta.getDeviceName()).withBridge(bridgeUID).withProperties(properties)
.withRepresentationProperty(DEVICE_PROP_DEVICE_MAC_ID).build();
}).forEach(this::thingDiscovered);
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto;
import static org.openhab.binding.vesync.internal.VeSyncConstants.DEFAULT_POLL_INTERVAL_AIR_FILTERS_DEVICES;
import static org.openhab.binding.vesync.internal.VeSyncConstants.DEFAULT_REFRESH_INTERVAL_DISCOVERED_DEVICES;
/**
* The {@link VeSyncBridgeConfiguration} is a container for all the bridge configuration.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncBridgeConfiguration {
public String username;
public String password;
public long airPurifierPollInterval = DEFAULT_POLL_INTERVAL_AIR_FILTERS_DEVICES;
public boolean backgroundDeviceDiscovery;
public long refreshBackgroundDeviceDiscovery = DEFAULT_REFRESH_INTERVAL_DISCOVERED_DEVICES;
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.requests;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncAuthenticatedRequest} is a Java class used as a DTO to hold the Vesync's API's common request data.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncAuthenticatedRequest extends VeSyncRequest {
@SerializedName("accountID")
public String accountId;
@SerializedName("token")
public String token;
public VeSyncAuthenticatedRequest() {
super();
}
public VeSyncAuthenticatedRequest(final VeSyncUserSession user) throws AuthenticationException {
super();
if (user == null) {
throw new AuthenticationException("User is not logged in");
}
this.token = user.getToken();
this.accountId = user.getAccountId();
}
public void applyAuthentication(final VeSyncUserSession userSession) throws AuthenticationException {
if (userSession == null) {
throw new AuthenticationException("User is not logged in");
}
this.accountId = userSession.getAccountId();
this.token = userSession.getToken();
}
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.requests;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncLoginCredentials} is the Java class as a DTO to hold login credentials for the Vesync
* API.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncLoginCredentials extends VeSyncRequest {
@SerializedName("email")
public String email;
@SerializedName("password")
public String passwordMd5;
@SerializedName("userType")
public String userType;
@SerializedName("devToken")
public String devToken = "";
public VeSyncLoginCredentials() {
super();
userType = "1";
method = "login";
}
public VeSyncLoginCredentials(String email, String password) {
this();
this.email = email;
this.passwordMd5 = password;
}
}

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.requests;
/**
* The {@link VeSyncProtocolConstants} contains common Strings used by various elements of the protocol.
*
* @author David Goodyear - Initial contribution
*/
public interface VeSyncProtocolConstants {
// Common Payloads
String MODE_AUTO = "auto";
String MODE_MANUAL = "manual";
String MODE_SLEEP = "sleep";
String MODE_ON = "on";
String MODE_DIM = "dim";
String MODE_OFF = "off";
// Common Commands
String DEVICE_SET_SWITCH = "setSwitch";
String DEVICE_SET_DISPLAY = "setDisplay";
String DEVICE_SET_LEVEL = "setLevel";
// Humidifier Commands
String DEVICE_SET_AUTOMATIC_STOP = "setAutomaticStop";
String DEVICE_SET_HUMIDITY_MODE = "setHumidityMode";
String DEVICE_SET_TARGET_HUMIDITY_MODE = "setTargetHumidity";
String DEVICE_SET_VIRTUAL_LEVEL = "setVirtualLevel";
String DEVICE_SET_NIGHT_LIGHT_BRIGHTNESS = "setNightLightBrightness";
String DEVICE_GET_HUMIDIFIER_STATUS = "getHumidifierStatus";
String DEVICE_LEVEL_TYPE_MIST = "mist";
// Air Purifier Commands
String DEVICE_SET_PURIFIER_MODE = "setPurifierMode";
String DEVICE_SET_CHILD_LOCK = "setChildLock";
String DEVICE_SET_NIGHT_LIGHT = "setNightLight";
String DEVICE_GET_PURIFIER_STATUS = "getPurifierStatus";
String DEVICE_LEVEL_TYPE_WIND = "wind";
/**
* Base URL for AUTHENTICATION REQUESTS
*/
String PROTOCOL = "https";
String HOST_ENDPOINT = PROTOCOL + "://smartapi.vesync.com/cloud";
String V1_LOGIN_ENDPOINT = HOST_ENDPOINT + "/v1/user/login";
String V1_MANAGED_DEVICES_ENDPOINT = HOST_ENDPOINT + "/v1/deviceManaged/devices";
String V2_BYPASS_ENDPOINT = HOST_ENDPOINT + "/v2/deviceManaged/bypassV2";
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.requests;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncRequest} is a Java class used as a DTO to hold the Vesync's API's common request data.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncRequest {
@SerializedName("timeZone")
public String timeZone = "America/New_York";
@SerializedName("acceptLanguage")
public String acceptLanguage = "en";
@SerializedName("appVersion")
public String appVersion = "2.5.1";
@SerializedName("phoneBrand")
public String phoneBrand = "SM N9005";
@SerializedName("phoneOS")
public String phoneOS = "Android";
@SerializedName("traceId")
public String traceId = "";
@SerializedName("method")
public String method;
public VeSyncRequest() {
traceId = String.valueOf(System.currentTimeMillis());
}
}

View File

@ -0,0 +1,164 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.requests;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncRequestManagedDeviceBypassV2} is a Java class used as a DTO to hold the Vesync's API's common
* request data for V2 ByPass payloads.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncRequestManagedDeviceBypassV2 extends VeSyncAuthenticatedRequest {
@SerializedName("deviceRegion")
public String deviceRegion = "";
@SerializedName("debugMode")
public boolean debugMode = false;
@SerializedName("cid")
public String cid = "";
@SerializedName("configModule")
public String configModule = "";
@SerializedName("payload")
public VesyncManagedDeviceBase payload = new VesyncManagedDeviceBase();
/**
* Contains basic information about the device.
*/
public class VesyncManagedDeviceBase {
@SerializedName("method")
public String method;
@SerializedName("source")
public String source = "APP";
@SerializedName("data")
public EmptyPayload data = new EmptyPayload();
}
public static class EmptyPayload {
}
public static class SetSwitchPayload extends EmptyPayload {
public SetSwitchPayload(final boolean enabled, final int id) {
this.enabled = enabled;
this.id = id;
}
@SerializedName("enabled")
public boolean enabled = true;
@SerializedName("id")
public int id = -1;
}
public static class EnabledPayload extends EmptyPayload {
public EnabledPayload(final boolean enabled) {
this.enabled = enabled;
}
@SerializedName("enabled")
public boolean enabled = true;
}
public static class SetLevelPayload extends EmptyPayload {
public SetLevelPayload(final int id, final String type, final int level) {
this.id = id;
this.type = type;
this.level = level;
}
@SerializedName("id")
public int id = -1;
@SerializedName("level")
public int level = -1;
@SerializedName("type")
public String type = "";
}
public static class SetState extends EmptyPayload {
public SetState(final boolean state) {
this.state = state;
}
@SerializedName("state")
public boolean state = false;
}
public static class SetNightLight extends EmptyPayload {
public SetNightLight(final String state) {
this.nightLight = state;
}
@SerializedName("night_light")
public String nightLight = "";
}
public static class SetNightLightBrightness extends EmptyPayload {
public SetNightLightBrightness(final int state) {
this.nightLightLevel = state;
}
@SerializedName("night_light_brightness")
public int nightLightLevel = 0;
}
public static class SetTargetHumidity extends EmptyPayload {
public SetTargetHumidity(final int state) {
this.targetHumidity = state;
}
@SerializedName("target_humidity")
public int targetHumidity = 0;
}
public static class SetChildLock extends EmptyPayload {
public SetChildLock(final boolean childLock) {
this.childLock = childLock;
}
@SerializedName("child_lock")
public boolean childLock = false;
}
public static class SetMode extends EmptyPayload {
public SetMode(final String mode) {
this.mode = mode;
}
@SerializedName("mode")
public String mode = "";
}
public VeSyncRequestManagedDeviceBypassV2() {
super();
method = "bypassV2";
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.requests;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncRequestManagedDevicesPage} is the Java class as a DTO to hold login credentials for the Vesync
* API.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncRequestManagedDevicesPage extends VeSyncAuthenticatedRequest {
@SerializedName("pageNo")
public String pageNo;
@SerializedName("pageSize")
public String pageSize;
public VeSyncRequestManagedDevicesPage(final VeSyncUserSession user) throws AuthenticationException {
super(user);
method = "devices";
}
public VeSyncRequestManagedDevicesPage(final VeSyncUserSession user, int pageNo, int pageSize)
throws AuthenticationException {
this(user);
this.pageNo = String.valueOf(pageNo);
this.pageSize = String.valueOf(pageSize);
}
public String getPageNo() {
return pageNo;
}
public String getPageSize() {
return pageSize;
}
}

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.requests;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncRequestV1ManagedDeviceDetails} is the Java class as a DTO to hold login credentials for the Vesync
* API.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncRequestV1ManagedDeviceDetails extends VeSyncAuthenticatedRequest {
@SerializedName("mobileId")
public String mobileId = "1234567890123456";
@SerializedName("uuid")
public String uuid = null;
public VeSyncRequestV1ManagedDeviceDetails(final String deviceUuid) {
uuid = deviceUuid;
method = "deviceDetail";
}
public VeSyncRequestV1ManagedDeviceDetails(final VeSyncUserSession user) throws AuthenticationException {
super(user);
method = "deviceDetail";
}
public VeSyncRequestV1ManagedDeviceDetails(final VeSyncUserSession user, String deviceUuid)
throws AuthenticationException {
this(user);
uuid = deviceUuid;
}
public String getUuid() {
return uuid;
}
public String getMobileId() {
return mobileId;
}
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.responses;
/**
* The {@link VeSyncLoginResponse} is a Java class used as a DTO to hold the Vesync's API's login response.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncLoginResponse extends VeSyncResponse {
public VeSyncUserSession result;
public VeSyncUserSession getUserSession() {
return result;
}
public String getToken() {
return (result == null) ? null : result.token;
}
public String getAccountId() {
return (result == null) ? null : result.accountId;
}
@Override
public String toString() {
return "VesyncLoginResponse [msg=" + getMsg() + ", result=" + result + "]";
}
}

View File

@ -0,0 +1,142 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.responses;
import com.google.gson.annotations.SerializedName;
/**
* Contains basic information about a single device, from within a VeSyncManagedDevicesPage.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncManagedDeviceBase {
@SerializedName("deviceRegion")
public String deviceRegion;
public String getDeviceRegion() {
return deviceRegion;
}
@SerializedName("deviceType")
public String deviceType;
public String getDeviceType() {
return deviceType;
}
@SerializedName("deviceName")
public String deviceName;
public String getDeviceName() {
return deviceName;
}
@SerializedName("deviceImg")
public String deviceImg;
public String getDeviceImg() {
return deviceImg;
}
@SerializedName("deviceStatus")
public String deviceStatus;
public String getDeviceStatus() {
return deviceStatus;
}
@SerializedName("cid")
public String cid;
public String getCid() {
return cid;
}
@SerializedName("connectionStatus")
public String connectionStatus;
public String getConnectionStatus() {
return connectionStatus;
}
@SerializedName("connectionType")
public String connectionType;
public String getConnectionType() {
return connectionType;
}
@SerializedName("type")
public String type;
public String getType() {
return type;
}
@SerializedName("subDeviceNo")
public String subDeviceNo;
public String getSubDeviceNo() {
return subDeviceNo;
}
@SerializedName("subDeviceType")
public String subDeviceType;
public String getSubDeviceType() {
return subDeviceType;
}
@SerializedName("uuid")
public String uuid;
public String getUuid() {
return uuid;
}
@SerializedName("macID")
public String macId;
public String getMacId() {
return macId;
}
@SerializedName("currentFirmVersion")
public String currentFirmVersion;
public String getCurrentFirmVersion() {
return currentFirmVersion;
}
@SerializedName("configModule")
public String configModule;
public String getConfigModule() {
return configModule;
}
@SerializedName("mode")
public String mode;
public String getMode() {
return mode;
}
@SerializedName("speed")
public String speed;
public String getSpeed() {
return speed;
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.responses;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncManagedDevicesPage} is a Java class used as a DTO to hold the Vesync's API's response data to a
* page of data requesting the manages devices.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncManagedDevicesPage extends VeSyncResponse {
@SerializedName("result")
public Outcome outcome;
public class Outcome {
@SerializedName("pageNo")
public String pageNo;
@SerializedName("total")
public String total;
@SerializedName("pageSize")
public String pageSize;
@SerializedName("list")
public VeSyncManagedDeviceBase[] list;
public String getPageNo() {
return pageNo;
}
public String getPageSize() {
return pageSize;
}
public String getTotal() {
return total;
}
}
}

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.responses;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncResponse} is a Java class used as a DTO to hold the Vesync's API's common response data.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncResponse {
@SerializedName("traceId")
public String traceId;
@SerializedName("code")
public String code;
@SerializedName("msg")
public String msg;
public String getMsg() {
return msg;
}
public String getTraceId() {
return traceId;
}
public String getCode() {
return code;
}
public boolean isMsgSuccess() {
return (msg != null) ? "request success".equals(msg) : false;
}
public boolean isMsgDeviceOffline() {
return (msg != null) ? "device offline".equals(msg) : false;
}
@Override
public String toString() {
return "VesyncResponse [traceId=\"" + traceId + "\", msg=\"" + msg + "\", code=\"" + code + "\"]";
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.responses;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncResponseManagedDeviceBypassV2} is a Java class used as a DTO to hold the Vesync's API's common
* response data.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncResponseManagedDeviceBypassV2 extends VeSyncResponse {
@SerializedName("result")
public ManagedDeviceByPassV2Payload result;
public class ManagedDeviceByPassV2Payload extends VeSyncResponse {
}
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.responses;
import com.google.gson.annotations.SerializedName;
/**
* Contains data about the logged in user - including the accountID and token's used
* for authenticating other payload's.
*
* @see unit test - Result may not be in respone if not authenticated
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncUserSession {
public String token;
public String getToken() {
return token;
}
@SerializedName("registerTime")
public String registerTime;
@SerializedName("accountID")
public String accountId;
public String getAccountId() {
return accountId;
}
@SerializedName("registerAppVersion")
public String registerAppVersion;
@SerializedName("countryCode")
public String countryCode;
@SerializedName("acceptLanguage")
public String acceptLanguage;
@Override
public String toString() {
return "Data [user=AB" + ", token=" + token + "]";
}
}

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.responses;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncV2BypassHumidifierStatus} is a Java class used as a DTO to hold the Vesync's API's common response
* data, in regards to a Air Humidifier device.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncV2BypassHumidifierStatus extends VeSyncResponse {
@SerializedName("result")
public HumidifierrStatus result;
public class HumidifierrStatus extends VeSyncResponse {
@SerializedName("result")
public AirHumidifierStatus result;
public class AirHumidifierStatus {
@SerializedName("enabled")
public boolean enabled;
@SerializedName("humidity")
public int humidity;
@SerializedName("mist_virtual_level")
public int mistVirtualLevel;
@SerializedName("mist_level")
public int mistLevel;
@SerializedName("mode")
public String mode;
@SerializedName("water_lacks")
public boolean waterLacks;
@SerializedName("humidity_high")
public boolean humidityHigh;
@SerializedName("water_tank_lifted")
public boolean waterTankLifted;
@SerializedName("display")
public boolean display;
@SerializedName("automatic_stop_reach_target")
public boolean automaticStopReachTarget;
@SerializedName("configuration")
public HumidityPurifierConfig configuration;
@SerializedName("night_light_brightness")
public int nightLightBrightness;
@SerializedName("warm_enabled")
public boolean warnEnabled;
@SerializedName("warm_level")
public int warmLevel;
public class HumidityPurifierConfig {
@SerializedName("auto_target_humidity")
public int autoTargetHumidity;
@SerializedName("display")
public boolean display;
@SerializedName("automatic_stop")
public boolean automaticStop;
}
}
}
}

View File

@ -0,0 +1,99 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.responses;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncV2BypassPurifierStatus} is a Java class used as a DTO to hold the Vesync's API's common response
* data,
* in regards to a Air Purifier device.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncV2BypassPurifierStatus extends VeSyncResponse {
@SerializedName("result")
public PurifierStatus result;
public class PurifierStatus extends VeSyncResponse {
@SerializedName("result")
public AirPurifierStatus result;
public class AirPurifierStatus {
@SerializedName("enabled")
public boolean enabled;
@SerializedName("filter_life")
public int filterLife;
@SerializedName("mode")
public String mode;
@SerializedName("level")
public int level;
@SerializedName("air_quality")
public int airQuality;
@SerializedName("air_quality_value")
public int airQualityValue;
@SerializedName("display")
public boolean display;
@SerializedName("child_lock")
public boolean childLock;
@SerializedName("night_light")
public String nightLight;
@SerializedName("configuration")
public AirPurifierConfig configuration;
public class AirPurifierConfig {
@SerializedName("display")
public boolean display;
@SerializedName("display_forever")
public boolean displayForever;
@SerializedName("auto_preference")
public AirPurifierConfigAutoPref autoPreference;
public class AirPurifierConfigAutoPref {
@SerializedName("type")
public String autoType;
@SerializedName("room_size")
public int roomSize;
}
}
@SerializedName("extension")
public AirPurifierExtension extension;
public class AirPurifierExtension {
@SerializedName("schedule_count")
public int scheduleCount;
@SerializedName("timer_remain")
public int timerRemain;
}
@SerializedName("device_error_code")
public int deviceErrorCode;
}
}
}

View File

@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.dto.responses.v1;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncResponse;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VeSyncV1AirPurifierDeviceDetailsResponse} is a Java class used as a DTO to hold the Vesync's V1 API's
* common response
* data, in regards to a Air Purifier device.
*
* @author David Goodyear - Initial contribution
*/
public class VeSyncV1AirPurifierDeviceDetailsResponse extends VeSyncResponse {
@SerializedName("screenStatus")
public String screenStatus;
public String getScreenStatus() {
return screenStatus;
}
@SerializedName("airQuality")
public int airQuality;
public int getAirQuality() {
return airQuality;
}
@SerializedName("level")
public int level;
public int getLevel() {
return level;
}
@SerializedName("mode")
public String mode;
public String getMode() {
return mode;
}
@SerializedName("deviceName")
public String deviceName;
public String getDeviceName() {
return deviceName;
}
@SerializedName("currentFirmVersion")
public String currentFirmVersion;
public String getCurrentFirmVersion() {
return currentFirmVersion;
}
@SerializedName("childLock")
public String childLock;
public String getChildLock() {
return childLock;
}
@SerializedName("deviceStatus")
public String deviceStatus;
public String getDeviceStatus() {
return deviceStatus;
}
@SerializedName("deviceImg")
public String deviceImgUrl;
public String getDeviceImgUrl() {
return deviceImgUrl;
}
@SerializedName("connectionStatus")
public String connectionStatus;
public String getConnectionStatus() {
return connectionStatus;
}
public boolean isDeviceOnline() {
return "online".equals(connectionStatus);
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link AuthenticationException} is thrown if the authentication/login process is unsuccessful.
*
* @author David Godyear - Initial contribution
*/
@NonNullByDefault
public class AuthenticationException extends Exception {
private static final long serialVersionUID = -7786425895604150557L;
public AuthenticationException() {
super();
}
public AuthenticationException(final String message) {
super(message);
}
public AuthenticationException(final Throwable cause) {
super(cause);
}
public AuthenticationException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link DeviceUnknownException} is thrown if the device information could not be located for the address in
* relation
* to the API.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class DeviceUnknownException extends Exception {
private static final long serialVersionUID = -7786425642285150557L;
public DeviceUnknownException() {
super();
}
public DeviceUnknownException(final String message) {
super(message);
}
public DeviceUnknownException(final Throwable cause) {
super(cause);
}
public DeviceUnknownException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,508 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handlers;
import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.V2_BYPASS_ENDPOINT;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
import org.openhab.binding.vesync.internal.VeSyncDeviceConfiguration;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VeSyncBaseDeviceHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public abstract class VeSyncBaseDeviceHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(VeSyncBaseDeviceHandler.class);
private static final String MARKER_INVALID_DEVICE_KEY = "---INVALID---";
@NotNull
protected String deviceLookupKey = MARKER_INVALID_DEVICE_KEY;
private static final int CACHE_TIMEOUT_SECOND = 5;
private int activePollRate = -2; // -1 is used to deactivate the poll, so default to a different value
private @Nullable ScheduledFuture<?> backgroundPollingScheduler;
private final Object pollConfigLock = new Object();
protected @Nullable VeSyncClient veSyncClient;
private volatile long latestReadBackMillis = 0;
@Nullable
ScheduledFuture<?> initialPollingTask = null;
@Nullable
ScheduledFuture<?> readbackPollTask = null;
public VeSyncBaseDeviceHandler(Thing thing) {
super(thing);
}
protected @Nullable Channel findChannelById(final String channelGroupId) {
return getThing().getChannel(channelGroupId);
}
protected ExpiringCache<String> lastPollResultCache = new ExpiringCache<>(Duration.ofSeconds(CACHE_TIMEOUT_SECOND),
VeSyncBaseDeviceHandler::expireCacheContents);
private static @Nullable String expireCacheContents() {
return null;
}
@Override
public void channelLinked(ChannelUID channelUID) {
super.channelLinked(channelUID);
scheduler.execute(this::pollForUpdate);
}
protected void setBackgroundPollInterval(final int seconds) {
if (activePollRate == seconds) {
return;
}
logger.debug("Reconfiguring devices background polling to {} seconds", seconds);
synchronized (pollConfigLock) {
final ScheduledFuture<?> job = backgroundPollingScheduler;
// Cancel the current scan's and re-schedule as required
if (job != null && !job.isCancelled()) {
job.cancel(true);
backgroundPollingScheduler = null;
}
if (seconds > 0) {
logger.trace("Device data is polling every {} seconds", seconds);
backgroundPollingScheduler = scheduler.scheduleWithFixedDelay(this::pollForUpdate, seconds, seconds,
TimeUnit.SECONDS);
}
activePollRate = seconds;
}
}
public boolean requiresMetaDataFrequentUpdates() {
return (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey));
}
private @Nullable BridgeHandler getBridgeHandler() {
Bridge bridgeRef = getBridge();
if (bridgeRef == null) {
return null;
} else {
return bridgeRef.getHandler();
}
}
protected boolean isDeviceOnline() {
BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
@Nullable
VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap().get(deviceLookupKey);
if (metadata == null) {
return false;
}
return ("online".equals(metadata.connectionStatus));
}
return false;
}
public void updateDeviceMetaData() {
Map<String, String> newProps = null;
BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
@Nullable
VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap().get(deviceLookupKey);
if (metadata == null) {
return;
}
newProps = getMetadataProperities(metadata);
// Refresh the device -> protocol mapping
deviceLookupKey = getValidatedIdString();
if ("online".equals(metadata.connectionStatus)) {
updateStatus(ThingStatus.ONLINE);
} else if ("offline".equals(metadata.connectionStatus)) {
updateStatus(ThingStatus.OFFLINE);
}
}
if (newProps != null && !newProps.isEmpty()) {
this.updateProperties(newProps);
removeChannels();
if (!isDeviceSupported()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Device Model or Type not supported by this thing");
}
}
}
/**
* Override this in classes that extend this, to
*/
protected void customiseChannels() {
}
protected String[] getChannelsToRemove() {
return new String[] {};
}
private void removeChannels() {
final String[] channelsToRemove = getChannelsToRemove();
final List<Channel> channelsToBeRemoved = new ArrayList<>();
for (String name : channelsToRemove) {
Channel ch = getThing().getChannel(name);
if (ch != null) {
channelsToBeRemoved.add(ch);
}
}
final ThingBuilder builder = editThing().withoutChannels(channelsToBeRemoved);
updateThing(builder.build());
}
/**
* Extract the common properties for all devices, from the given meta-data of a device.
*
* @param metadata - the meta-data of a device
* @return - Map of common props
*/
public Map<String, String> getMetadataProperities(final @Nullable VeSyncManagedDeviceBase metadata) {
if (metadata == null) {
return Map.of();
}
final Map<String, String> newProps = new HashMap<>(4);
newProps.put(DEVICE_PROP_DEVICE_MAC_ID, metadata.getMacId());
newProps.put(DEVICE_PROP_DEVICE_NAME, metadata.getDeviceName());
newProps.put(DEVICE_PROP_DEVICE_TYPE, metadata.getDeviceType());
newProps.put(DEVICE_PROP_DEVICE_UUID, metadata.getUuid());
return newProps;
}
protected synchronized @Nullable VeSyncClient getVeSyncClient() {
if (veSyncClient == null) {
Bridge bridge = getBridge();
if (bridge == null) {
return null;
}
ThingHandler handler = bridge.getHandler();
if (handler instanceof VeSyncClient) {
veSyncClient = (VeSyncClient) handler;
} else {
return null;
}
}
return veSyncClient;
}
protected void requestBridgeFreqScanMetadataIfReq() {
if (requiresMetaDataFrequentUpdates()) {
BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
vesyncBridgeHandler.checkIfIncreaseScanRateRequired();
}
}
}
@NotNull
public String getValidatedIdString() {
final VeSyncDeviceConfiguration config = getConfigAs(VeSyncDeviceConfiguration.class);
BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null && bridgeHandler instanceof VeSyncBridgeHandler) {
VeSyncBridgeHandler vesyncBridgeHandler = (VeSyncBridgeHandler) bridgeHandler;
final String configMac = config.macId;
// Try to use the mac directly
if (configMac != null) {
logger.debug("Searching for device mac id : {}", configMac);
@Nullable
VeSyncManagedDeviceBase metadata = vesyncBridgeHandler.api.getMacLookupMap()
.get(configMac.toLowerCase());
if (metadata != null && metadata.macId != null) {
return metadata.macId;
}
}
final String deviceName = config.deviceName;
// Check if the device name can be matched to a single device
if (deviceName != null) {
final String[] matchedMacIds = vesyncBridgeHandler.api.getMacLookupMap().values().stream()
.filter(x -> deviceName.equals(x.deviceName)).map(x -> x.macId).toArray(String[]::new);
for (String val : matchedMacIds) {
logger.debug("Found MAC match on name with : {}", val);
}
if (matchedMacIds.length != 1) {
return MARKER_INVALID_DEVICE_KEY;
}
if (vesyncBridgeHandler.api.getMacLookupMap().get(matchedMacIds[0]) != null) {
return matchedMacIds[0];
}
}
}
return MARKER_INVALID_DEVICE_KEY;
}
@Override
public void initialize() {
intializeDeviceForUse();
}
private void intializeDeviceForUse() {
// Sanity check basic setup
final VeSyncBridgeHandler bridge = (VeSyncBridgeHandler) getBridgeHandler();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Missing bridge for API link");
return;
} else {
updateStatus(ThingStatus.UNKNOWN);
}
deviceLookupKey = getValidatedIdString();
// Populate device props - this is required for polling, to cross-check the device model.
updateDeviceMetaData();
// If the base device class marks it as offline there is an issue that will prevent normal operation
if (getThing().getStatus().equals(ThingStatus.OFFLINE)) {
return;
}
// This will force the bridge to push the configuration parameters for polling to the handler
bridge.updateThing(this);
// Give the bridge time to build the datamaps of the devices
scheduleInitialPoll();
}
private void scheduleInitialPoll() {
cancelInitialPoll(false);
initialPollingTask = scheduler.schedule(this::pollForUpdate, 10, TimeUnit.SECONDS);
}
private void cancelInitialPoll(final boolean interruptAllowed) {
final ScheduledFuture<?> pollJob = initialPollingTask;
if (pollJob != null && !pollJob.isCancelled()) {
pollJob.cancel(interruptAllowed);
initialPollingTask = null;
}
}
private void cancelReadbackPoll(final boolean interruptAllowed) {
final ScheduledFuture<?> pollJob = readbackPollTask;
if (pollJob != null && !pollJob.isCancelled()) {
pollJob.cancel(interruptAllowed);
readbackPollTask = null;
}
}
@Override
public void dispose() {
cancelReadbackPoll(true);
cancelInitialPoll(true);
}
public void pollForUpdate() {
pollForDeviceData(lastPollResultCache);
}
/**
* This should be implemented by subclasses to provide the implementation for polling the specific
* data for the type the class is responsible for. (Excluding meta data).
*
* @param cachedResponse - An Expiring cache that can be utilised to store the responses, to prevent poll bursts by
* coalescing the requests.
*/
protected abstract void pollForDeviceData(final ExpiringCache<String> cachedResponse);
/**
* Send a BypassV2 command to the device. The body of the response is returned, a poll is done if the request
* should have been dispatched.
*
* @param method - the V2 bypass method
* @param payload - The payload to send in within the V2 bypass command
* @return - The body of the response, or EMPTY_STRING if the command could not be issued.
*/
protected final String sendV2BypassControlCommand(final String method,
final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload) {
return sendV2BypassControlCommand(method, payload, true);
}
/**
* Send a BypassV2 command to the device. The body of the response is returned.
*
* @param method - the V2 bypass method
* @param payload - The payload to send in within the V2 bypass command
* @param readbackDevice - if set to true after the command has been issued, whether a poll of the devices data
* should be run.
* @return - The body of the response, or EMPTY_STRING if the command could not be issued.
*/
protected final String sendV2BypassControlCommand(final String method,
final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload, final boolean readbackDevice) {
final String result = sendV2BypassCommand(method, payload);
if (!result.equals(EMPTY_STRING) && readbackDevice) {
performReadbackPoll();
}
return result;
}
public final String sendV1Command(final String method, final String url, final VeSyncAuthenticatedRequest request) {
if (ThingStatus.OFFLINE.equals(this.thing.getStatus())) {
logger.debug("Command blocked as device is offline");
return EMPTY_STRING;
}
try {
if (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)) {
deviceLookupKey = getValidatedIdString();
}
VeSyncClient client = getVeSyncClient();
if (client != null) {
return client.reqV2Authorized(url, deviceLookupKey, request);
} else {
throw new DeviceUnknownException("Missing client");
}
} catch (AuthenticationException e) {
logger.debug("Auth exception {}", e.getMessage());
return EMPTY_STRING;
} catch (final DeviceUnknownException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Check configuration details - " + e.getMessage());
// In case the name is updated server side - request the scan rate is increased
requestBridgeFreqScanMetadataIfReq();
return EMPTY_STRING;
}
}
/**
* Send a BypassV2 command to the device. The body of the response is returned.
*
* @param method - the V2 bypass method
* @param payload - The payload to send in within the V2 bypass command
* @return - The body of the response, or EMPTY_STRING if the command could not be issued.
*/
protected final String sendV2BypassCommand(final String method,
final VeSyncRequestManagedDeviceBypassV2.EmptyPayload payload) {
if (ThingStatus.OFFLINE.equals(this.thing.getStatus())) {
logger.debug("Command blocked as device is offline");
return EMPTY_STRING;
}
VeSyncRequestManagedDeviceBypassV2 readReq = new VeSyncRequestManagedDeviceBypassV2();
readReq.payload.method = method;
readReq.payload.data = payload;
try {
if (MARKER_INVALID_DEVICE_KEY.equals(deviceLookupKey)) {
deviceLookupKey = getValidatedIdString();
}
VeSyncClient client = getVeSyncClient();
if (client != null) {
return client.reqV2Authorized(V2_BYPASS_ENDPOINT, deviceLookupKey, readReq);
} else {
throw new DeviceUnknownException("Missing client");
}
} catch (AuthenticationException e) {
logger.debug("Auth exception {}", e.getMessage());
return EMPTY_STRING;
} catch (final DeviceUnknownException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Check configuration details - " + e.getMessage());
// In case the name is updated server side - request the scan rate is increased
requestBridgeFreqScanMetadataIfReq();
return EMPTY_STRING;
}
}
// Given several changes may be done at the same time, or in close proximity, delay the read-back to catch
// multiple read-back's, so a single update can handle them.
public void performReadbackPoll() {
final long requestSystemMillis = System.currentTimeMillis();
latestReadBackMillis = requestSystemMillis;
cancelReadbackPoll(false);
readbackPollTask = scheduler.schedule(() -> {
// This is a historical poll, ignore it
if (requestSystemMillis != latestReadBackMillis) {
logger.trace("Poll read-back cancelled, another later one is scheduled to happen");
return;
}
logger.trace("Read-back poll executing");
// Read-backs should never use the cached data - but may provide it for poll's that coincide with
// the caches alive duration.
lastPollResultCache.invalidateValue();
pollForUpdate();
}, 1L, TimeUnit.SECONDS);
}
public void updateBridgeBasedPolls(VeSyncBridgeConfiguration config) {
}
/**
* Subclasses should implement this method, and return true if the device is a model it can support
* interoperability with. If it cannot be determind to be a mode
*
* @return - true if the device is supported, false if the device isn't. E.g. Unknown model id in meta-data would
* return false.
*/
protected abstract boolean isDeviceSupported();
}

View File

@ -0,0 +1,241 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handlers;
import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
import org.openhab.binding.vesync.internal.api.IHttpClientProvider;
import org.openhab.binding.vesync.internal.api.VeSyncV2ApiHelper;
import org.openhab.binding.vesync.internal.discovery.DeviceMetaDataUpdatedHandler;
import org.openhab.binding.vesync.internal.discovery.VeSyncDiscoveryService;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VeSyncBridgeHandler} is responsible for handling the bridge things created to use the VeSync
* API. This way, the user credentials may be entered only once.
*
* @author David Goodyear - Initial Contribution
*/
@NonNullByDefault
public class VeSyncBridgeHandler extends BaseBridgeHandler implements VeSyncClient {
private static final int DEFAULT_DEVICE_SCAN_INTERVAL = 600;
private static final int DEFAULT_DEVICE_SCAN_RECOVERY_INTERVAL = 60;
private static final int DEFAULT_DEVICE_SCAN_DISABLED = -1;
private final Logger logger = LoggerFactory.getLogger(VeSyncBridgeHandler.class);
private @Nullable ScheduledFuture<?> backgroundDiscoveryPollingJob;
protected final VeSyncV2ApiHelper api = new VeSyncV2ApiHelper();
private IHttpClientProvider httpClientProvider;
private volatile int backgroundScanTime = -1;
private final Object scanConfigLock = new Object();
public VeSyncBridgeHandler(Bridge bridge, @NotNull IHttpClientProvider httpClientProvider) {
super(bridge);
this.httpClientProvider = httpClientProvider;
}
public ThingUID getUID() {
return thing.getUID();
}
protected void checkIfIncreaseScanRateRequired() {
logger.trace("Checking if increased background scanning for new devices / base information is required");
boolean frequentScanReq = false;
for (Thing th : getThing().getThings()) {
ThingHandler handler = th.getHandler();
if (handler instanceof VeSyncBaseDeviceHandler) {
if (((VeSyncBaseDeviceHandler) handler).requiresMetaDataFrequentUpdates()) {
frequentScanReq = true;
break;
}
}
}
if (!frequentScanReq
&& api.getMacLookupMap().values().stream().anyMatch(x -> "offline".equals(x.connectionStatus))) {
frequentScanReq = true;
}
if (frequentScanReq) {
setBackgroundScanInterval(DEFAULT_DEVICE_SCAN_RECOVERY_INTERVAL);
} else {
setBackgroundScanInterval(DEFAULT_DEVICE_SCAN_INTERVAL);
}
}
protected void setBackgroundScanInterval(final int seconds) {
synchronized (scanConfigLock) {
ScheduledFuture<?> job = backgroundDiscoveryPollingJob;
if (backgroundScanTime != seconds) {
if (seconds > 0) {
logger.trace("Scheduling background scanning for new devices / base information every {} seconds",
seconds);
} else {
logger.trace("Disabling background scanning for new devices / base information");
}
// Cancel the current scan's and re-schedule as required
if (job != null && !job.isCancelled()) {
job.cancel(true);
backgroundDiscoveryPollingJob = null;
}
if (seconds > 0) {
backgroundDiscoveryPollingJob = scheduler.scheduleWithFixedDelay(
this::runDeviceScanSequenceNoAuthErrors, seconds, seconds, TimeUnit.SECONDS);
}
backgroundScanTime = seconds;
}
}
}
public void registerMetaDataUpdatedHandler(DeviceMetaDataUpdatedHandler dmduh) {
handlers.add(dmduh);
}
public void unregisterMetaDataUpdatedHandler(DeviceMetaDataUpdatedHandler dmduh) {
handlers.remove(dmduh);
}
private final CopyOnWriteArrayList<DeviceMetaDataUpdatedHandler> handlers = new CopyOnWriteArrayList<>();
public void runDeviceScanSequenceNoAuthErrors() {
try {
runDeviceScanSequence();
updateStatus(ThingStatus.ONLINE);
} catch (AuthenticationException ae) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check login credentials");
}
}
public void runDeviceScanSequence() throws AuthenticationException {
logger.trace("Scanning for new devices / base information now");
api.discoverDevices();
handlers.forEach(x -> x.handleMetadataRetrieved(this));
checkIfIncreaseScanRateRequired();
this.updateThings();
}
public java.util.stream.Stream<@NotNull VeSyncManagedDeviceBase> getAirPurifiersMetadata() {
return api.getMacLookupMap().values().stream()
.filter(x -> VeSyncDeviceAirPurifierHandler.SUPPORTED_DEVICE_TYPES.contains(x.deviceType));
}
public java.util.stream.Stream<@NotNull VeSyncManagedDeviceBase> getAirHumidifiersMetadata() {
return api.getMacLookupMap().values().stream()
.filter(x -> VeSyncDeviceAirHumidifierHandler.SUPPORTED_DEVICE_TYPES.contains(x.deviceType));
}
protected void updateThings() {
final VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class);
getThing().getThings().forEach((th) -> updateThing(config, th.getHandler()));
}
public void updateThing(ThingHandler handler) {
final VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class);
updateThing(config, handler);
}
private void updateThing(VeSyncBridgeConfiguration config, @Nullable ThingHandler handler) {
if (handler instanceof VeSyncBaseDeviceHandler) {
((VeSyncBaseDeviceHandler) handler).updateDeviceMetaData();
((VeSyncBaseDeviceHandler) handler).updateBridgeBasedPolls(config);
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(VeSyncDiscoveryService.class);
}
@Override
public void initialize() {
api.setHttpClient(httpClientProvider.getHttpClient());
VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class);
scheduler.submit(() -> {
final String passwordMd5 = VeSyncV2ApiHelper.calculateMd5(config.password);
try {
api.login(config.username, passwordMd5, "Europe/London");
api.updateBridgeData(this);
runDeviceScanSequence();
updateStatus(ThingStatus.ONLINE);
} catch (final AuthenticationException ae) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check login credentials");
// The background scan will keep trying to authenticate in case the users credentials are updated on the
// veSync servers,
// to match the binding's configuration.
}
});
}
@Override
public void dispose() {
setBackgroundScanInterval(DEFAULT_DEVICE_SCAN_DISABLED);
api.setHttpClient(null);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.warn("Handling command for VeSync bridge handler.");
}
public void handleNewUserSession(final @Nullable VeSyncUserSession userSessionData) {
final Map<String, String> newProps = new HashMap<>();
if (userSessionData != null) {
newProps.put(DEVICE_PROP_BRIDGE_REG_TS, userSessionData.registerTime);
newProps.put(DEVICE_PROP_BRIDGE_COUNTRY_CODE, userSessionData.countryCode);
newProps.put(DEVICE_PROP_BRIDGE_ACCEPT_LANG, userSessionData.acceptLanguage);
}
this.updateProperties(newProps);
}
public String reqV2Authorized(final String url, final String macId, final VeSyncAuthenticatedRequest requestData)
throws AuthenticationException, DeviceUnknownException {
return api.reqV2Authorized(url, macId, requestData);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handlers;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException;
/**
* The {@link VeSyncClient} is TBC.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public interface VeSyncClient {
String reqV2Authorized(final String url, final String macId, final VeSyncAuthenticatedRequest requestData)
throws AuthenticationException, DeviceUnknownException;
}

View File

@ -0,0 +1,349 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handlers;
import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassHumidifierStatus;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VeSyncDeviceAirHumidifierHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
public static final int DEFAULT_AIR_PURIFIER_POLL_RATE = 120;
// "Device Type" values
public static final String DEV_TYPE_DUAL_200S = "Dual200S";
public static final String DEV_TYPE_CLASSIC_200S = "Classic200S";
public static final String DEV_TYPE_CORE_301S = "LUH-D301S-WEU";
public static final String DEV_TYPE_CLASSIC_300S = "Classic300S";
public static final String DEV_TYPE_600S = "LUH-A602S-WUS";
public static final String DEV_TYPE_600S_EU = "LUH-A602S-WEU";
private static final List<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);
public static final List<String> SUPPORTED_DEVICE_TYPES = List.of(DEV_TYPE_DUAL_200S, DEV_TYPE_CLASSIC_200S,
DEV_TYPE_CLASSIC_300S, DEV_TYPE_CORE_301S, DEV_TYPE_600S, DEV_TYPE_600S_EU);
private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirHumidifierHandler.class);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIR_HUMIDIFIER);
private final Object pollLock = new Object();
public VeSyncDeviceAirHumidifierHandler(Thing thing) {
super(thing);
}
@Override
protected String[] getChannelsToRemove() {
String[] toRemove = new String[] {};
final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
if (deviceType != null) {
switch (deviceType) {
case DEV_TYPE_CLASSIC_300S:
toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL };
break;
case DEV_TYPE_DUAL_200S:
case DEV_TYPE_CLASSIC_200S:
case DEV_TYPE_CORE_301S:
toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL,
DEVICE_CHANNEL_AF_NIGHT_LIGHT };
break;
case DEV_TYPE_600S:
case DEV_TYPE_600S_EU:
toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT };
break;
}
}
return toRemove;
}
@Override
public void initialize() {
super.initialize();
customiseChannels();
}
@Override
public void updateBridgeBasedPolls(final VeSyncBridgeConfiguration config) {
Integer pollRate = config.airPurifierPollInterval;
if (pollRate == null) {
pollRate = DEFAULT_AIR_PURIFIER_POLL_RATE;
}
if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
setBackgroundPollInterval(-1);
} else {
setBackgroundPollInterval(pollRate);
}
}
@Override
public void dispose() {
this.setBackgroundPollInterval(-1);
}
@Override
protected boolean isDeviceSupported() {
final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
if (deviceType == null) {
return false;
}
return SUPPORTED_DEVICE_TYPES.contains(deviceType);
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
if (deviceType == null) {
return;
}
scheduler.submit(() -> {
if (command instanceof OnOffType) {
switch (channelUID.getId()) {
case DEVICE_CHANNEL_ENABLED:
sendV2BypassControlCommand(DEVICE_SET_SWITCH,
new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload(command.equals(OnOffType.ON),
0));
break;
case DEVICE_CHANNEL_DISPLAY_ENABLED:
sendV2BypassControlCommand(DEVICE_SET_DISPLAY,
new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON)));
break;
case DEVICE_CHANNEL_STOP_AT_TARGET:
sendV2BypassControlCommand(DEVICE_SET_AUTOMATIC_STOP,
new VeSyncRequestManagedDeviceBypassV2.EnabledPayload(command.equals(OnOffType.ON)));
break;
case DEVICE_CHANNEL_WARM_ENABLED:
logger.warn("Warm mode API is unknown in order to send the command");
break;
}
} else if (command instanceof QuantityType) {
switch (channelUID.getId()) {
case DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY:
int targetHumidity = ((QuantityType<?>) command).intValue();
if (targetHumidity < 30) {
logger.warn("Target Humidity less than 30 - adjusting to 30 as the valid API value");
targetHumidity = 30;
} else if (targetHumidity > 80) {
logger.warn("Target Humidity greater than 80 - adjusting to 80 as the valid API value");
targetHumidity = 80;
}
sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_AUTO), false);
sendV2BypassControlCommand(DEVICE_SET_TARGET_HUMIDITY_MODE,
new VeSyncRequestManagedDeviceBypassV2.SetTargetHumidity(targetHumidity));
break;
case DEVICE_CHANNEL_MIST_LEVEL:
int targetMistLevel = ((QuantityType<?>) command).intValue();
// If more devices have this the hope is it's those with the prefix LUH so the check can
// be simplified, originally devices mapped 1/5/9 to 1/2/3.
if (DEV_TYPE_CORE_301S.equals(deviceType)) {
if (targetMistLevel < 1) {
logger.warn("Target Mist Level less than 1 - adjusting to 1 as the valid API value");
targetMistLevel = 1;
} else if (targetMistLevel > 2) {
logger.warn("Target Mist Level greater than 2 - adjusting to 2 as the valid API value");
targetMistLevel = 2;
}
} else {
if (targetMistLevel < 1) {
logger.warn("Target Mist Level less than 1 - adjusting to 1 as the valid API value");
targetMistLevel = 1;
} else if (targetMistLevel > 3) {
logger.warn("Target Mist Level greater than 3 - adjusting to 3 as the valid API value");
targetMistLevel = 3;
}
// Re-map to what appears to be bitwise encoding of the states
switch (targetMistLevel) {
case 1:
targetMistLevel = 1;
break;
case 2:
targetMistLevel = 5;
break;
case 3:
targetMistLevel = 9;
break;
}
}
sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false);
sendV2BypassControlCommand(DEVICE_SET_VIRTUAL_LEVEL,
new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_MIST,
targetMistLevel));
break;
case DEVICE_CHANNEL_WARM_LEVEL:
logger.warn("Warm level API is unknown in order to send the command");
break;
}
} else if (command instanceof StringType) {
final String targetMode = command.toString().toLowerCase();
switch (channelUID.getId()) {
case DEVICE_CHANNEL_HUMIDIFIER_MODE:
if (!CLASSIC_300S_600S_MODES.contains(targetMode)) {
logger.warn(
"Humidifier mode command for \"{}\" is not valid in the (Classic300S/600S) API possible options {}",
command, String.join(",", CLASSIC_300S_NIGHT_LIGHT_MODES));
return;
}
sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
new VeSyncRequestManagedDeviceBypassV2.SetMode(targetMode));
break;
case DEVICE_CHANNEL_AF_NIGHT_LIGHT:
if (!DEV_TYPE_CLASSIC_300S.equals(deviceType) && !DEV_TYPE_CORE_301S.equals(deviceType)) {
logger.warn("Humidifier night light is not valid for your device ({}})", deviceType);
return;
}
if (!CLASSIC_300S_NIGHT_LIGHT_MODES.contains(targetMode)) {
logger.warn(
"Humidifier night light mode command for \"{}\" is not valid in the (Classic300S) API possible options {}",
command, String.join(",", CLASSIC_300S_NIGHT_LIGHT_MODES));
return;
}
int targetValue;
switch (targetMode) {
case MODE_OFF:
targetValue = 0;
break;
case MODE_DIM:
targetValue = 50;
break;
case MODE_ON:
targetValue = 100;
break;
default:
return; // should never hit
}
sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT_BRIGHTNESS,
new VeSyncRequestManagedDeviceBypassV2.SetNightLightBrightness(targetValue));
}
} else if (command instanceof RefreshType) {
pollForUpdate();
} else {
logger.trace("UNKNOWN COMMAND: {} {}", command.getClass().toString(), channelUID);
}
});
}
@Override
protected void pollForDeviceData(final ExpiringCache<String> cachedResponse) {
String response;
VeSyncV2BypassHumidifierStatus humidifierStatus;
synchronized (pollLock) {
response = cachedResponse.getValue();
boolean cachedDataUsed = response != null;
if (response == null) {
logger.trace("Requesting fresh response");
response = sendV2BypassCommand(DEVICE_GET_HUMIDIFIER_STATUS,
new VeSyncRequestManagedDeviceBypassV2.EmptyPayload());
} else {
logger.trace("Using cached response {}", response);
}
if (response.equals(EMPTY_STRING)) {
return;
}
humidifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassHumidifierStatus.class);
if (humidifierStatus == null) {
return;
}
if (!cachedDataUsed) {
cachedResponse.putValue(response);
}
}
// Bail and update the status of the thing - it will be updated to online by the next search
// that detects it is online.
if (humidifierStatus.isMsgDeviceOffline()) {
updateStatus(ThingStatus.OFFLINE);
return;
} else if (humidifierStatus.isMsgSuccess()) {
updateStatus(ThingStatus.ONLINE);
}
if (!"0".equals(humidifierStatus.result.getCode())) {
logger.warn("Check correct Thing type has been set - API gave a unexpected response for an Air Humidifier");
return;
}
final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(humidifierStatus.result.result.enabled));
updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(humidifierStatus.result.result.display));
updateState(DEVICE_CHANNEL_WATER_LACKS, OnOffType.from(humidifierStatus.result.result.waterLacks));
updateState(DEVICE_CHANNEL_HUMIDITY_HIGH, OnOffType.from(humidifierStatus.result.result.humidityHigh));
updateState(DEVICE_CHANNEL_WATER_TANK_LIFTED, OnOffType.from(humidifierStatus.result.result.waterTankLifted));
updateState(DEVICE_CHANNEL_STOP_AT_TARGET,
OnOffType.from(humidifierStatus.result.result.automaticStopReachTarget));
updateState(DEVICE_CHANNEL_HUMIDITY,
new QuantityType<>(humidifierStatus.result.result.humidity, Units.PERCENT));
updateState(DEVICE_CHANNEL_MIST_LEVEL, new DecimalType(humidifierStatus.result.result.mistLevel));
updateState(DEVICE_CHANNEL_HUMIDIFIER_MODE, new StringType(humidifierStatus.result.result.mode));
// Only the 300S supports nightlight currently of tested devices.
if (DEV_TYPE_CLASSIC_300S.equals(deviceType) || DEV_TYPE_CORE_301S.equals(deviceType)) {
// Map the numeric that only applies to the same modes as the Air Filter 300S series.
if (humidifierStatus.result.result.nightLightBrightness == 0) {
updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_OFF));
} else if (humidifierStatus.result.result.nightLightBrightness == 100) {
updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_ON));
} else {
updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_DIM));
}
} else if (DEV_TYPE_600S.equals(deviceType) || DEV_TYPE_600S_EU.equals(deviceType)) {
updateState(DEVICE_CHANNEL_WARM_ENABLED, OnOffType.from(humidifierStatus.result.result.warnEnabled));
updateState(DEVICE_CHANNEL_WARM_LEVEL, new DecimalType(humidifierStatus.result.result.warmLevel));
}
updateState(DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY,
new QuantityType<>(humidifierStatus.result.result.configuration.autoTargetHumidity, Units.PERCENT));
}
}

View File

@ -0,0 +1,423 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handlers;
import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2;
import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestV1ManagedDeviceDetails;
import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassPurifierStatus;
import org.openhab.binding.vesync.internal.dto.responses.v1.VeSyncV1AirPurifierDeviceDetailsResponse;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.library.items.DateTimeItem;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VeSyncDeviceAirPurifierHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
public static final int DEFAULT_AIR_PURIFIER_POLL_RATE = 120;
// "Device Type" values
public static final String DEV_TYPE_CORE_600S = "LAP-C601S-WUS";
public static final String DEV_TYPE_CORE_400S = "Core400S";
public static final String DEV_TYPE_CORE_300S = "Core300S";
public static final String DEV_TYPE_CORE_201S = "LAP-C201S-AUSR";
public static final String DEV_TYPE_CORE_200S = "Core200S";
public static final String DEV_TYPE_LV_PUR131S = "LV-PUR131S";
public static final List<String> SUPPORTED_DEVICE_TYPES = Arrays.asList(DEV_TYPE_CORE_600S, DEV_TYPE_CORE_400S,
DEV_TYPE_CORE_300S, DEV_TYPE_CORE_201S, DEV_TYPE_CORE_200S, DEV_TYPE_LV_PUR131S);
private static final List<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);
private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirPurifierHandler.class);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIR_PURIFIER);
private final Object pollLock = new Object();
public VeSyncDeviceAirPurifierHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
customiseChannels();
}
@Override
protected @NotNull String[] getChannelsToRemove() {
String[] toRemove = new String[] {};
final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
if (deviceType != null) {
switch (deviceType) {
case DEV_TYPE_CORE_600S:
case DEV_TYPE_CORE_400S:
toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT };
break;
case DEV_TYPE_LV_PUR131S:
toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT, DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE,
DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF, DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME,
DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING, DEVICE_CHANNEL_AIRQUALITY_PM25,
DEVICE_CHANNEL_AF_SCHEDULES_COUNT, DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER };
break;
default:
toRemove = new String[] { DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, DEVICE_CHANNEL_AF_SCHEDULES_COUNT };
}
}
return toRemove;
}
@Override
public void updateBridgeBasedPolls(final VeSyncBridgeConfiguration config) {
Integer pollRate = config.airPurifierPollInterval;
if (pollRate == null) {
pollRate = DEFAULT_AIR_PURIFIER_POLL_RATE;
}
if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
setBackgroundPollInterval(-1);
} else {
setBackgroundPollInterval(pollRate);
}
}
@Override
public void dispose() {
this.setBackgroundPollInterval(-1);
}
@Override
protected boolean isDeviceSupported() {
final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
if (deviceType == null) {
return false;
}
return SUPPORTED_DEVICE_TYPES.contains(deviceType);
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
if (deviceType == null) {
return;
}
scheduler.submit(() -> {
if (command instanceof OnOffType) {
switch (channelUID.getId()) {
case DEVICE_CHANNEL_ENABLED:
sendV2BypassControlCommand(DEVICE_SET_SWITCH,
new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload(command.equals(OnOffType.ON),
0));
break;
case DEVICE_CHANNEL_DISPLAY_ENABLED:
sendV2BypassControlCommand(DEVICE_SET_DISPLAY,
new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON)));
break;
case DEVICE_CHANNEL_CHILD_LOCK_ENABLED:
sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK,
new VeSyncRequestManagedDeviceBypassV2.SetChildLock(command.equals(OnOffType.ON)));
break;
}
} else if (command instanceof StringType) {
switch (channelUID.getId()) {
case DEVICE_CHANNEL_FAN_MODE_ENABLED:
final String targetFanMode = command.toString().toLowerCase();
switch (deviceType) {
case DEV_TYPE_CORE_600S:
case DEV_TYPE_CORE_400S:
if (!CORE_400S600S_FAN_MODES.contains(targetFanMode)) {
logger.warn(
"Fan mode command for \"{}\" is not valid in the (Core400S) API possible options {}",
command, String.join(",", CORE_400S600S_FAN_MODES));
return;
}
break;
case DEV_TYPE_CORE_200S:
case DEV_TYPE_CORE_201S:
case DEV_TYPE_CORE_300S:
if (!CORE_200S300S_FAN_MODES.contains(targetFanMode)) {
logger.warn(
"Fan mode command for \"{}\" is not valid in the (Core200S/Core300S) API possible options {}",
command, String.join(",", CORE_200S300S_FAN_MODES));
return;
}
break;
}
sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE,
new VeSyncRequestManagedDeviceBypassV2.SetMode(targetFanMode));
break;
case DEVICE_CHANNEL_AF_NIGHT_LIGHT:
final String targetNightLightMode = command.toString().toLowerCase();
switch (deviceType) {
case DEV_TYPE_CORE_600S:
case DEV_TYPE_CORE_400S:
logger.warn("Core400S API does not support night light");
return;
case DEV_TYPE_CORE_200S:
case DEV_TYPE_CORE_201S:
case DEV_TYPE_CORE_300S:
if (!CORE_200S300S_NIGHT_LIGHT_MODES.contains(targetNightLightMode)) {
logger.warn(
"Night light mode command for \"{}\" is not valid in the (Core200S/Core300S) API possible options {}",
command, String.join(",", CORE_200S300S_NIGHT_LIGHT_MODES));
return;
}
sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT,
new VeSyncRequestManagedDeviceBypassV2.SetNightLight(targetNightLightMode));
break;
}
break;
}
} else if (command instanceof QuantityType) {
switch (channelUID.getId()) {
case DEVICE_CHANNEL_FAN_SPEED_ENABLED:
// If the fan speed is being set enforce manual mode
sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE,
new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false);
int requestedLevel = ((QuantityType<?>) command).intValue();
if (requestedLevel < 1) {
logger.warn("Fan speed command less than 1 - adjusting to 1 as the valid API value");
requestedLevel = 1;
}
switch (deviceType) {
case DEV_TYPE_CORE_600S:
case DEV_TYPE_CORE_400S:
if (requestedLevel > 4) {
logger.warn(
"Fan speed command greater than 4 - adjusting to 4 as the valid (Core400S) API value");
requestedLevel = 4;
}
break;
case DEV_TYPE_CORE_200S:
case DEV_TYPE_CORE_201S:
case DEV_TYPE_CORE_300S:
if (requestedLevel > 3) {
logger.warn(
"Fan speed command greater than 3 - adjusting to 3 as the valid (Core200S/Core300S) API value");
requestedLevel = 3;
}
break;
}
sendV2BypassControlCommand(DEVICE_SET_LEVEL,
new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_WIND,
requestedLevel));
break;
}
} else if (command instanceof RefreshType) {
pollForUpdate();
} else {
logger.trace("UNKNOWN COMMAND: {} {}", command.getClass().toString(), channelUID);
}
});
}
@Override
protected void pollForDeviceData(final ExpiringCache<String> cachedResponse) {
final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
if (deviceType == null) {
return;
}
switch (deviceType) {
case DEV_TYPE_CORE_600S:
case DEV_TYPE_CORE_400S:
case DEV_TYPE_CORE_300S:
case DEV_TYPE_CORE_201S:
case DEV_TYPE_CORE_200S:
processV2BypassPoll(cachedResponse);
break;
case DEV_TYPE_LV_PUR131S:
processV1AirPurifierPoll(cachedResponse);
break;
}
}
private void processV1AirPurifierPoll(final ExpiringCache<String> cachedResponse) {
final String deviceUuid = getThing().getProperties().get(DEVICE_PROP_DEVICE_UUID);
if (deviceUuid == null) {
return;
}
String response;
VeSyncV1AirPurifierDeviceDetailsResponse purifierStatus;
synchronized (pollLock) {
response = cachedResponse.getValue();
boolean cachedDataUsed = response != null;
if (response == null) {
logger.trace("Requesting fresh response");
response = sendV1Command("POST", "https://smartapi.vesync.com/131airPurifier/v1/device/deviceDetail",
new VeSyncRequestV1ManagedDeviceDetails(deviceUuid));
} else {
logger.trace("Using cached response {}", response);
}
if (response.equals(EMPTY_STRING)) {
return;
}
purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV1AirPurifierDeviceDetailsResponse.class);
if (purifierStatus == null) {
return;
}
if (!cachedDataUsed) {
cachedResponse.putValue(response);
}
}
// Bail and update the status of the thing - it will be updated to online by the next search
// that detects it is online.
if (purifierStatus.isDeviceOnline()) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE);
return;
}
if (!"0".equals(purifierStatus.getCode())) {
logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier");
return;
}
updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(MODE_ON.equals(purifierStatus.getDeviceStatus())));
updateState(DEVICE_CHANNEL_CHILD_LOCK_ENABLED, OnOffType.from(MODE_ON.equals(purifierStatus.getChildLock())));
updateState(DEVICE_CHANNEL_FAN_MODE_ENABLED, new StringType(purifierStatus.getMode()));
updateState(DEVICE_CHANNEL_FAN_SPEED_ENABLED, new DecimalType(String.valueOf(purifierStatus.getLevel())));
updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(MODE_ON.equals(purifierStatus.getScreenStatus())));
updateState(DEVICE_CHANNEL_AIRQUALITY_BASIC, new DecimalType(purifierStatus.getAirQuality()));
}
private void processV2BypassPoll(final ExpiringCache<String> cachedResponse) {
String response;
VeSyncV2BypassPurifierStatus purifierStatus;
synchronized (pollLock) {
response = cachedResponse.getValue();
boolean cachedDataUsed = response != null;
if (response == null) {
logger.trace("Requesting fresh response");
response = sendV2BypassCommand(DEVICE_GET_PURIFIER_STATUS,
new VeSyncRequestManagedDeviceBypassV2.EmptyPayload());
} else {
logger.trace("Using cached response {}", response);
}
if (response.equals(EMPTY_STRING)) {
return;
}
purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassPurifierStatus.class);
if (purifierStatus == null) {
return;
}
if (!cachedDataUsed) {
cachedResponse.putValue(response);
}
}
// Bail and update the status of the thing - it will be updated to online by the next search
// that detects it is online.
if (purifierStatus.isMsgDeviceOffline()) {
updateStatus(ThingStatus.OFFLINE);
return;
} else if (purifierStatus.isMsgSuccess()) {
updateStatus(ThingStatus.ONLINE);
}
if (!"0".equals(purifierStatus.result.getCode())) {
logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier");
return;
}
updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(purifierStatus.result.result.enabled));
updateState(DEVICE_CHANNEL_CHILD_LOCK_ENABLED, OnOffType.from(purifierStatus.result.result.childLock));
updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(purifierStatus.result.result.display));
updateState(DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING,
new QuantityType<>(purifierStatus.result.result.filterLife, Units.PERCENT));
updateState(DEVICE_CHANNEL_FAN_MODE_ENABLED, new StringType(purifierStatus.result.result.mode));
updateState(DEVICE_CHANNEL_FAN_SPEED_ENABLED, new DecimalType(purifierStatus.result.result.level));
updateState(DEVICE_CHANNEL_ERROR_CODE, new DecimalType(purifierStatus.result.result.deviceErrorCode));
updateState(DEVICE_CHANNEL_AIRQUALITY_BASIC, new DecimalType(purifierStatus.result.result.airQuality));
updateState(DEVICE_CHANNEL_AIRQUALITY_PM25,
new QuantityType<>(purifierStatus.result.result.airQualityValue, Units.MICROGRAM_PER_CUBICMETRE));
updateState(DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER,
OnOffType.from(purifierStatus.result.result.configuration.displayForever));
updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF,
new StringType(purifierStatus.result.result.configuration.autoPreference.autoType));
updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE,
new DecimalType(purifierStatus.result.result.configuration.autoPreference.roomSize));
// Only 400S appears to have this JSON extension object
if (purifierStatus.result.result.extension != null) {
if (purifierStatus.result.result.extension.timerRemain > 0) {
updateState(DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, new DateTimeType(LocalDateTime.now()
.plus(purifierStatus.result.result.extension.timerRemain, ChronoUnit.SECONDS).toString()));
} else {
updateState(DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, new DateTimeItem("nullEnforcements").getState());
}
updateState(DEVICE_CHANNEL_AF_SCHEDULES_COUNT,
new DecimalType(purifierStatus.result.result.extension.scheduleCount));
}
// Not applicable to 400S payload's
if (purifierStatus.result.result.nightLight != null) {
updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new DecimalType(purifierStatus.result.result.nightLight));
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="vesync" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>VeSync Binding</name>
<description>This is the binding for the VeSync products. Currently, this supports the Levoit branded Air Purifiers and
Humidifiers using the v2 protocol.</description>
</binding:binding>

View File

@ -0,0 +1,334 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="vesync"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="bridge">
<label>VeSync Bridge</label>
<description>The VeSync bridge represents the VeSync cloud service.</description>
<properties>
<property name="Registration Time"/>
<property name="Country Code"/>
<property name="Accept Language"/>
</properties>
<config-description>
<parameter name="username" type="text">
<context>email</context>
<required>true</required>
<label>Username</label>
<description>Name of a registered VeSync user, that allows to access the mobile application.</description>
</parameter>
<parameter name="password" type="text">
<context>password</context>
<required>true</required>
<label>Password</label>
<description>Password for the registered VeSync username, that allows to access the mobile application.</description>
</parameter>
<parameter name="backgroundDeviceDiscovery" type="boolean">
<label>Background Device Scans</label>
<description>Enable background scanning for new devices.</description>
<default>true</default>
</parameter>
<parameter name="airPurifierPollInterval" type="integer" min="5" step="1" unit="s">
<label>Air Filters/Humidifiers Poll Rate</label>
<description>Seconds between fetching background updates about the air purifiers / humidifiers.</description>
<default>60</default>
</parameter>
</config-description>
</bridge-type>
<thing-type id="airPurifier">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>Air Purifier via VeSync</label>
<description>A Air Purifier uplinking to VeSync</description>
<channels>
<channel id="enabled" typeId="deviceEnabledType"/>
<channel id="childLock" typeId="deviceChildLockEnabledType"/>
<channel id="display" typeId="deviceDisplayEnabledType"/>
<channel id="filterLifePercentage" typeId="deviceFilterLifePercentageType"/>
<channel id="fanMode" typeId="airPurifierModeType"/>
<channel id="manualFanSpeed" typeId="airPurifierFanLevelType"/>
<channel id="errorCode" typeId="deviceErrorCodeType"/>
<channel id="airQuality" typeId="deviceAirQualityBasicType"/>
<channel id="airQualityPM25" typeId="airQualityPM25"/>
<channel id="configDisplayForever" typeId="deviceAFConfigDisplayForever"/>
<channel id="configAutoMode" typeId="deviceAFConfigAutoPrefType"/>
<channel id="timerExpiry" typeId="deviceAFTimerExpiry"/>
<channel id="configAutoRoomSize" typeId="deviceAFConfigAutoPrefRoomSizeType"/>
<channel id="schedulesCount" typeId="deviceAFConfigAutoScheduleCountType"/>
<channel id="nightLightMode" typeId="deviceAFNightLight"/>
</channels>
<properties>
<property name="Device Name"/>
<property name="Device Type"/>
<property name="MAC Id"/>
</properties>
<representation-property>macId</representation-property>
<config-description>
<parameter name="macId" type="text">
<label>MAC Id</label>
<description>The MAC Id of the device as reported by the API.</description>
</parameter>
<parameter name="deviceName" type="text">
<label>Device Name</label>
<description>The name allocated to the device by the app. (Must be unique if used)</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="airHumidifier">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>Air Humidifier via VeSync</label>
<description>A Air Humidifier uplinking to VeSync</description>
<channels>
<channel id="enabled" typeId="deviceEnabledType"/>
<channel id="display" typeId="deviceDisplayEnabledType"/>
<channel id="waterLacking" typeId="deviceWaterLackingType"/>
<channel id="humidityHigh" typeId="deviceHighHumidityType"/>
<channel id="waterTankLifted" typeId="deviceWaterTankLiftedType"/>
<channel id="stopAtHumiditySetpoint" typeId="deviceAutomaticStopReachTargetType"/>
<channel id="humidity" typeId="deviceHumidityType"/>
<channel id="mistLevel" typeId="deviceMistLevelType"/>
<channel id="humidifierMode" typeId="airHumidifierModeType"/>
<channel id="nightLightMode" typeId="deviceAFNightLight"/>
<channel id="humiditySetpoint" typeId="deviceConfigTargetHumidity"/>
<channel id="warmEnabled" typeId="warmModeEnabled"/>
<channel id="warmLevel" typeId="warmLevel"/>
</channels>
<properties>
<property name="Device Name"/>
<property name="Device Type"/>
<property name="MAC Id"/>
</properties>
<representation-property>macId</representation-property>
<config-description>
<parameter name="macId" type="text">
<label>MAC Id</label>
<description>The MAC Id of the device as reported by the API.</description>
</parameter>
<parameter name="deviceName" type="text">
<label>Device Name</label>
<description>The name allocated to the device by the app. (Must be unique if used)</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="deviceEnabledType">
<item-type>Switch</item-type>
<label>Switched On</label>
<description>Indicator if the device is switched on</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="deviceChildLockEnabledType">
<item-type>Switch</item-type>
<label>Display Lock</label>
<description>Indicator if the devices child lock is enabled (Display Lock)</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="deviceDisplayEnabledType">
<item-type>Switch</item-type>
<label>Display</label>
<description>Indicator if the devices display is enabled</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="deviceFilterLifePercentageType">
<item-type>Number:Dimensionless</item-type>
<label>Filter Life Remaining</label>
<description>Indicator of the remaining filter life</description>
<state readOnly="true" pattern="%.0f %%"/>
</channel-type>
<channel-type id="airPurifierModeType">
<item-type>String</item-type>
<label>Operation Mode</label>
<description>The operating mode the air purifier is currently set to</description>
<state readOnly="false">
<options>
<option value="auto">Auto</option>
<option value="manual">Manual Fan Control</option>
<option value="sleep">Sleeping Auto</option>
</options>
</state>
</channel-type>
<channel-type id="deviceAFNightLight">
<item-type>String</item-type>
<label>Night Light</label>
<description>The operating mode of the night light functionality</description>
<state readOnly="false">
<options>
<option value="on">On</option>
<option value="dim">Dimmed</option>
<option value="off">Off</option>
</options>
</state>
</channel-type>
<channel-type id="airPurifierFanLevelType">
<item-type>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>
<label>Device Error Code</label>
<description>Indicator of the current error code of the device</description>
<state readOnly="true" pattern="%.0f"/>
</channel-type>
<channel-type id="deviceAirQualityBasicType">
<item-type>Number:Dimensionless</item-type>
<label>Air Quality</label>
<description>System representation of air quality</description>
<state readOnly="true" pattern="%.0f"/>
</channel-type>
<channel-type id="airQualityPM25">
<item-type>Number:Density</item-type>
<label>Air Quality PPM2.5</label>
<description>Indicator of current air quality</description>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="deviceAFConfigDisplayForever">
<item-type>Switch</item-type>
<label>Config: Display Forever</label>
<description>Configuration: If the devices display is enabled forever</description>
</channel-type>
<channel-type id="deviceAFConfigAutoPrefType">
<item-type>String</item-type>
<label>Config: Auto Mode</label>
<description>The operating mode when the air purifier is set to auto</description>
<state readOnly="true">
<options>
<option value="default">Auto (Air Quality)</option>
<option value="quiet">Quiet (No High Speed)</option>
<option value="efficient">Auto (Room Size)</option>
</options>
</state>
</channel-type>
<channel-type id="deviceAFTimerExpiry">
<item-type>DateTime</item-type>
<label>Auto Off Expiry</label>
<description>The time when the auto off timer will be reached</description>
<state readOnly="true" pattern="%1$tF %1$tR"/>
</channel-type>
<channel-type id="deviceAFConfigAutoPrefRoomSizeType">
<item-type>Number:Dimensionless</item-type>
<label>Config: Room size</label>
<description>Room size (foot sq) for efficient auto mode</description>
<state readOnly="true" pattern="%.0f sq ft"/>
</channel-type>
<channel-type id="deviceAFConfigAutoScheduleCountType">
<item-type>Number:Dimensionless</item-type>
<label>Config: Schedules Count</label>
<description>The current number of schedules configured</description>
<state readOnly="true" pattern="%.0f"/>
</channel-type>
<channel-type id="deviceWaterLackingType">
<item-type>Switch</item-type>
<label>Water Low/Empty</label>
<description>Indicator if the devices water is low or empty</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="deviceHighHumidityType">
<item-type>Switch</item-type>
<label>High Humidity</label>
<description>Indicator if the device is measuring high humidity</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="deviceWaterTankLiftedType">
<item-type>Switch</item-type>
<label>Water Tank Removed</label>
<description>Indicator if the device is reporting the water tank as removed</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="deviceAutomaticStopReachTargetType">
<item-type>Switch</item-type>
<label>Stop @ Set Point</label>
<description>Indicator if the device is set to stop when the humidity set point has been reached</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="deviceHumidityType">
<item-type>Number:Dimensionless</item-type>
<label>Humidity Level</label>
<description>System representation of humidity</description>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="deviceConfigTargetHumidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity Set Point</label>
<description>Humidity Set Point</description>
<state readOnly="false" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="deviceMistLevelType">
<item-type>Number:Dimensionless</item-type>
<label>Mist Level</label>
<description>System representation of mist level</description>
<state readOnly="false" pattern="%.0f"/>
</channel-type>
<channel-type id="airHumidifierModeType">
<item-type>String</item-type>
<label>Operation Mode</label>
<description>The operating mode the air humidifier is currently set to</description>
<state readOnly="false">
<options>
<option value="auto">Auto</option>
<option value="manual">Manual Control</option>
<option value="sleep">Sleeping Auto</option>
</options>
</state>
</channel-type>
<channel-type id="warmModeEnabled">
<item-type>Switch</item-type>
<label>Warm Mode Enabled</label>
<description>Indicator if the device is set to warm mist</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="warmLevel">
<item-type>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,74 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handler.requests;
import static org.junit.jupiter.api.Assertions.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.requests.VesyncAuthenticatedRequest;
import org.openhab.binding.vesync.internal.dto.requests.VesyncLoginCredentials;
import org.openhab.binding.vesync.internal.dto.responses.VesyncLoginResponse;
/**
* The {@link VesyncLoginCredentials} class implements unit test case for {@link VesyncLoginResponse}
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VesyncAuthenticatedRequestTest {
public final static VesyncLoginResponse.VesyncUserSession testUser = VeSyncConstants.GSON.fromJson(
org.openhab.binding.vesync.internal.handler.responses.VesyncLoginResponseTest.testGoodLoginResponseBody,
VesyncLoginResponse.class).result;
@Test
public void checkBaseFieldsExist() {
String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5"));
assertEquals(true, content.contains("\"timeZone\": \"America/New_York\""));
assertEquals(true, content.contains("\"acceptLanguage\": \"en\""));
assertEquals(true, content.contains("\"appVersion\": \"2.5.1\""));
assertEquals(true, content.contains("\"phoneBrand\": \"SM N9005\""));
assertEquals(true, content.contains("\"phoneOS\": \"Android\""));
Pattern p = Pattern.compile("\"traceId\": \"\\d+\"");
Matcher m = p.matcher(content);
assertEquals(true, m.find());
}
@Test
public void checkAuthenicationData() {
// Simulate as the code flow should run - parse data and then use it
VesyncLoginResponse response = VeSyncConstants.GSON
.fromJson(org.openhab.binding.vesync.internal.handler.responses.VesyncLoginResponseTest.testGoodLoginResponseBody, VesyncLoginResponse.class);
String content = "";
try {
content = VeSyncConstants.GSON.toJson(new VesyncAuthenticatedRequest(response.result));
} catch (AuthenticationException ae) {
}
assertEquals(true, content.contains("\"token\": \"AccessTokenString=\""));
assertEquals(true, content.contains("\"accountID\": \"5328043\""));
}
}

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handler.requests;
import static org.junit.jupiter.api.Assertions.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.api.VesyncV2ApiHelper;
import org.openhab.binding.vesync.internal.dto.requests.VesyncLoginCredentials;
/**
* The {@link VesyncLoginCredentials} class implements unit test case for {@link VesyncLoginResponse}
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VesyncLoginCredentialsTest {
@Test
public void checkMd5Calculation() {
assertEquals("577441848f056cd02d4c500b25fdd76a",VesyncV2ApiHelper.calculateMd5("TestHashInPythonLib=+"));
}
@Test
public void checkBaseFieldsExist() {
String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5"));
assertEquals(true, content.contains("\"timeZone\": \"America/New_York\""));
assertEquals(true, content.contains("\"acceptLanguage\": \"en\""));
assertEquals(true, content.contains("\"appVersion\": \"2.5.1\""));
assertEquals(true, content.contains("\"phoneBrand\": \"SM N9005\""));
assertEquals(true, content.contains("\"phoneOS\": \"Android\""));
Pattern p = Pattern.compile("\"traceId\": \"\\d+\"");
Matcher m = p.matcher(content);
assertEquals(true, m.find());
}
@Test
public void checkLoginMethodJson() {
String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5"));
assertEquals(true, content.contains("\"method\": \"login\""));
assertEquals(true, content.contains("\"email\": \"username\""));
assertEquals(true, content.contains("\"password\": \"passmd5\""));
assertEquals(true, content.contains("\"userType\": \"1\""));
assertEquals(true, content.contains("\"devToken\": \"\""));
}
}

View File

@ -0,0 +1,137 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handler.requests;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.api.VesyncV2ApiHelper;
import org.openhab.binding.vesync.internal.dto.requests.VesyncLoginCredentials;
import org.openhab.binding.vesync.internal.dto.requests.VesyncRequestManagedDeviceBypassV2;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* The {@link VesyncLoginCredentials} class implements unit test case for {@link VesyncLoginResponse}
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VesyncRequestManagedDeviceBypassV2Test {
@Test
public void checkMd5Calculation() {
assertEquals("577441848f056cd02d4c500b25fdd76a",VesyncV2ApiHelper.calculateMd5("TestHashInPythonLib=+"));
}
@Test
public void checkBaseFieldsExist() {
String content = VeSyncConstants.GSON.toJson(new VesyncRequestManagedDeviceBypassV2());
assertEquals(true, content.contains("\"method\": \"bypassV2\""));
assertEquals(true, content.contains("\"data\": {}"));
}
@Test
public void checkEmptyPayload() {
final VesyncRequestManagedDeviceBypassV2.EmptyPayload testPaylaod = new VesyncRequestManagedDeviceBypassV2.EmptyPayload();
final String contentTest1 = VeSyncConstants.GSON.toJson(testPaylaod);
assertEquals(true, contentTest1.equals("{}"));
}
@Test
public void checkSetLevelPayload() {
final VesyncRequestManagedDeviceBypassV2.SetLevelPayload testPaylaod = new VesyncRequestManagedDeviceBypassV2.SetLevelPayload(1,"stringval",2);
final String contentTest1 = VeSyncConstants.GSON.toJson(testPaylaod);
assertEquals(true, contentTest1.contains("\"id\": 1"));
assertEquals(true,contentTest1.contains("\"type\": \"stringval\""));
assertEquals(true,contentTest1.contains("\"level\": 2"));
}
@Test
public void checkSetChildLockPayload() {
final VesyncRequestManagedDeviceBypassV2.SetChildLock testPaylaod = new VesyncRequestManagedDeviceBypassV2.SetChildLock(false);
final String contentTest1 = VeSyncConstants.GSON.toJson(testPaylaod);
assertEquals(true,contentTest1.contains("\"child_lock\": false"));
testPaylaod.childLock = true;
final String contentTest2 = VeSyncConstants.GSON.toJson(testPaylaod);
assertEquals(true,contentTest2.contains("\"child_lock\": true"));
}
@Test
public void checkSetSwitchPayload() {
final VesyncRequestManagedDeviceBypassV2.SetSwitchPayload testPaylaod = new VesyncRequestManagedDeviceBypassV2.SetSwitchPayload(true,0);
final String contentTest1 = VeSyncConstants.GSON.toJson(testPaylaod);
assertEquals(true, contentTest1.contains("\"enabled\": true"));
assertEquals(true, contentTest1.contains("\"id\": 0"));
testPaylaod.enabled = false;
testPaylaod.id = 100;
final String contentTest2 = VeSyncConstants.GSON.toJson(testPaylaod);
assertEquals(true, contentTest2.contains("\"enabled\": false"));
assertEquals(true, contentTest2.contains("\"id\": 100"));
}
@Test
public void checkSetNightLightPayload() {
final VesyncRequestManagedDeviceBypassV2.SetNightLight testPaylaod = new VesyncRequestManagedDeviceBypassV2.SetNightLight("myValue");
final String contentTest1 = VeSyncConstants.GSON.toJson(testPaylaod);
assertEquals(true, contentTest1.contains("\"night_light\": \"myValue\""));
}
@Test
public void checkSetTargetHumidityPayload() {
final VesyncRequestManagedDeviceBypassV2.SetTargetHumidity test0Paylaod = new VesyncRequestManagedDeviceBypassV2.SetTargetHumidity(0);
final String contentTest1 = VeSyncConstants.GSON.toJson(test0Paylaod);
assertEquals(true, contentTest1.contains("\"target_humidity\": 0"));
final VesyncRequestManagedDeviceBypassV2.SetTargetHumidity test100Paylaod = new VesyncRequestManagedDeviceBypassV2.SetTargetHumidity(100);
final String contentTest2 = VeSyncConstants.GSON.toJson(test100Paylaod);
assertEquals(true, contentTest2.contains("\"target_humidity\": 100"));
}
@Test
public void checkSetNightLightBrightnessPayload() {
final VesyncRequestManagedDeviceBypassV2.SetNightLightBrightness test0Paylaod = new VesyncRequestManagedDeviceBypassV2.SetNightLightBrightness(0);
final String contentTest1 = VeSyncConstants.GSON.toJson(test0Paylaod);
assertEquals(true, contentTest1.contains("\"night_light_brightness\": 0"));
final VesyncRequestManagedDeviceBypassV2.SetNightLightBrightness test100Paylaod = new VesyncRequestManagedDeviceBypassV2.SetNightLightBrightness(100);
final String contentTest2 = VeSyncConstants.GSON.toJson(test100Paylaod);
assertEquals(true, contentTest2.contains("\"night_light_brightness\": 100"));
}
@Test
public void checkEnabledPayload() {
final VesyncRequestManagedDeviceBypassV2.EnabledPayload enabledOn = new VesyncRequestManagedDeviceBypassV2.EnabledPayload(true);
final String contentTest1 = VeSyncConstants.GSON.toJson(enabledOn);
assertEquals(true, contentTest1.contains("\"enabled\": true"));
final VesyncRequestManagedDeviceBypassV2.EnabledPayload enabledOff = new VesyncRequestManagedDeviceBypassV2.EnabledPayload(false);
final String contentTest2 = VeSyncConstants.GSON.toJson(enabledOff);
assertEquals(true, contentTest2.contains("\"enabled\": false"));
}
@Test
public void checkLoginMethodJson() {
String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5"));
assertEquals(true, content.contains("\"method\": \"login\""));
assertEquals(true, content.contains("\"email\": \"username\""));
assertEquals(true, content.contains("\"password\": \"passmd5\""));
assertEquals(true, content.contains("\"userType\": \"1\""));
assertEquals(true, content.contains("\"devToken\": \"\""));
}
}

View File

@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handler.requests;
import static org.junit.jupiter.api.Assertions.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.requests.VesyncLoginCredentials;
import org.openhab.binding.vesync.internal.dto.requests.VesyncRequestManagedDevicesPage;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The {@link VesyncRequestManagedDevicesPageTest} class implements unit test case for
* {@link VesyncRequestManagedDevicesPage}
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VesyncRequestManagedDevicesPageTest {
@Test
public void checkBaseFieldsExist() {
String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5"));
assertEquals(true, content.contains("\"timeZone\": \"America/New_York\""));
assertEquals(true, content.contains("\"acceptLanguage\": \"en\""));
assertEquals(true, content.contains("\"appVersion\": \"2.5.1\""));
assertEquals(true, content.contains("\"phoneBrand\": \"SM N9005\""));
assertEquals(true, content.contains("\"phoneOS\": \"Android\""));
Pattern p = Pattern.compile("\"traceId\": \"\\d+\"");
Matcher m = p.matcher(content);
assertEquals(true, m.find());
}
@Test
public void checkRequestDevicesFields() {
String content = "";
try {
content = VeSyncConstants.GSON
.toJson(new VesyncRequestManagedDevicesPage(org.openhab.binding.vesync.internal.handler.requests.VesyncAuthenticatedRequestTest.testUser, 1, 100));
} catch (AuthenticationException ae) {
}
assertEquals(true, content.contains("\"method\": \"devices\""));
assertEquals(true, content.contains("\"pageNo\": \"1\""));
assertEquals(true, content.contains("\"pageSize\": \"100\""));
assertEquals(true, content.contains("\"token\": \"AccessTokenString=\""));
assertEquals(true, content.contains("\"accountID\": \"5328043\""));
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package java.org.openhab.binding.vesync.internal.handler.requests;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.requests.VesyncRequest;
/**
* The {@link VesyncRequestTest} class implements unit test case for {@link VesyncRequest}
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VesyncRequestTest {
@Test
public void checkBaseFieldsExist() {
String content = VeSyncConstants.GSON.toJson(new VesyncRequest());
assertEquals(true, content.contains("\"timeZone\": \"America/New_York\""));
assertEquals(true, content.contains("\"acceptLanguage\": \"en\""));
assertEquals(true, content.contains("\"appVersion\": \"2.5.1\""));
assertEquals(true, content.contains("\"phoneBrand\": \"SM N9005\""));
assertEquals(true, content.contains("\"phoneOS\": \"Android\""));
Pattern p = Pattern.compile("\"traceId\": \"\\d+\"");
Matcher m = p.matcher(content);
assertEquals(true, m.find());
}
}

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handler.requests;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.requests.VesyncLoginCredentials;
import org.openhab.binding.vesync.internal.dto.requests.VesyncRequestManagedDevicesPage;
import org.openhab.binding.vesync.internal.dto.requests.VesyncRequestV1ManagedDeviceDetails;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* The {@link VesyncRequestV1ManagedDeviceDetails} class implements unit test case for
* {@link VesyncRequestManagedDevicesPage}
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VesyncRequestV1ManagedDeviceDetailsTest {
// Verified content URLS
// https://smartapi.vesync.com/131airPurifier/v1/device/deviceDetail
@Test
public void checkBaseFieldsExist() {
String content = VeSyncConstants.GSON.toJson(new VesyncLoginCredentials("username", "passmd5"));
assertEquals(true, content.contains("\"timeZone\": \"America/New_York\""));
assertEquals(true, content.contains("\"acceptLanguage\": \"en\""));
assertEquals(true, content.contains("\"appVersion\": \"2.5.1\""));
assertEquals(true, content.contains("\"phoneBrand\": \"SM N9005\""));
assertEquals(true, content.contains("\"phoneOS\": \"Android\""));
Pattern p = Pattern.compile("\"traceId\": \"\\d+\"");
Matcher m = p.matcher(content);
assertEquals(true, m.find());
}
@Test
public void checkRequestDevicesFields() {
String content = "";
try {
content = VeSyncConstants.GSON
.toJson(new VesyncRequestV1ManagedDeviceDetails(VesyncAuthenticatedRequestTest.testUser, "MyDeviceUUID"));
} catch (AuthenticationException ae) {
}
assertEquals(true, content.contains("\"uuid\": \"MyDeviceUUID\""));
assertEquals(true, content.contains("\"mobileId\": \"1234567890123456\""));
assertEquals(true, content.contains("\"token\": \"AccessTokenString=\""));
assertEquals(true, content.contains("\"accountID\": \"5328043\""));
}
}

View File

@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handler.responses;
import static org.junit.jupiter.api.Assertions.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.responses.VesyncLoginResponse;
/**
* The {@link VesyncLoginResponseTest} class implements unit test case for {@link VesyncLoginResponse}
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VesyncLoginResponseTest {
public final static String testGoodLoginResponseBody = "{\r\n" + " \"traceId\": \"1634253816\",\r\n"
+ " \"code\": 0,\r\n" + " \"msg\": \"request success\",\r\n" + " \"result\": {\r\n"
+ " \"isRequiredVerify\": true,\r\n" + " \"accountID\": \"5328043\",\r\n"
+ " \"avatarIcon\": \"https://image.vesync.com/defaultImages/user/avatar_nor.png\",\r\n"
+ " \"birthday\": \"\",\r\n" + " \"gender\": \"\",\r\n"
+ " \"acceptLanguage\": \"en\",\r\n" + " \"userType\": \"1\",\r\n"
+ " \"nickName\": \"david.goodyear\",\r\n" + " \"mailConfirmation\": true,\r\n"
+ " \"termsStatus\": true,\r\n" + " \"gdprStatus\": true,\r\n"
+ " \"countryCode\": \"GB\",\r\n" + " \"registerAppVersion\": \"VeSync 3.1.37 build3\",\r\n"
+ " \"registerTime\": \"2021-10-14 17:35:50\",\r\n"
+ " \"verifyEmail\": \"david.goodyear@gmail.com\",\r\n" + " \"heightCm\": 0.0,\r\n"
+ " \"weightTargetSt\": 0.0,\r\n" + " \"heightUnit\": \"FT\",\r\n"
+ " \"heightFt\": 0.0,\r\n" + " \"weightTargetKg\": 0.0,\r\n"
+ " \"weightTargetLb\": 0.0,\r\n" + " \"weightUnit\": \"LB\",\r\n"
+ " \"targetBfr\": 0.0,\r\n" + " \"displayFlag\": [],\r\n"
+ " \"real_weight_kg\": 0.0,\r\n" + " \"real_weight_lb\": 0.0,\r\n"
+ " \"real_weight_unit\": \"lb\",\r\n" + " \"heart_rate_zones\": 0.0,\r\n"
+ " \"run_step_long_cm\": 0.0,\r\n" + " \"walk_step_long_cm\": 0.0,\r\n"
+ " \"step_target\": 0.0,\r\n" + " \"sleep_target_mins\": 0.0,\r\n"
+ " \"token\": \"AccessTokenString=\"\r\n" + " }\r\n" + "}";
@Test
public void testParseLoginGoodResponse() {
VesyncLoginResponse response = VeSyncConstants.GSON.fromJson(testGoodLoginResponseBody,
VesyncLoginResponse.class);
if (response != null) {
assertEquals("1634253816", response.getTraceId());
assertEquals("AccessTokenString=", response.result.token);
assertEquals("request success", response.msg);
assertEquals("5328043", response.result.accountId);
assertEquals("VeSync 3.1.37 build3", response.result.registerAppVersion);
assertEquals("GB", response.result.countryCode);
assertTrue(response.isMsgSuccess());
} else {
fail("GSON returned null");
}
}
@Test
public void testParseLoginFailResponse() {
String testReponse = "{\r\n" + " \"traceId\": \"1634253816\",\r\n" + " \"code\": -11201022,\r\n"
+ " \"msg\": \"password incorrect\",\r\n" + " \"result\": null\r\n" + "}";
VesyncLoginResponse response = VeSyncConstants.GSON.fromJson(testReponse,
VesyncLoginResponse.class);
if (response != null) {
assertEquals("password incorrect", response.msg);
assertFalse(response.isMsgSuccess());
} else {
fail("GSON returned null");
}
}
}

View File

@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handler.responses;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.responses.VesyncLoginResponse;
import org.openhab.binding.vesync.internal.dto.responses.VesyncManagedDevicesPage;
import static org.junit.jupiter.api.Assertions.*;
/**
* The {@link VesyncManagedDevicesPageTest} class implements unit test case for {@link VesyncLoginResponse}
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VesyncManagedDevicesPageTest {
public final static String testGoodSearchResponsePageBody = "{\n" +
" \"traceId\": \"1634387642\",\n" +
" \"code\": 0,\n" +
" \"msg\": \"request success\",\n" +
" \"result\": {\n" +
" \"total\": 2,\n" +
" \"pageSize\": 100,\n" +
" \"pageNo\": 1,\n" +
" \"list\": [\n" +
" {\n" +
" \"deviceRegion\": \"EU\",\n" +
" \"isOwner\": true,\n" +
" \"authKey\": null,\n" +
" \"deviceName\": \"Air Filter\",\n" +
" \"deviceImg\": \"https://image.vesync.com/defaultImages/Core_400S_Series/icon_core400s_purifier_160.png\",\n" +
" \"cid\": \"cidValue1\",\n" +
" \"deviceStatus\": \"on\",\n" +
" \"connectionStatus\": \"online\",\n" +
" \"connectionType\": \"WiFi+BTOnboarding+BTNotify\",\n" +
" \"deviceType\": \"Core400S\",\n" +
" \"type\": \"wifi-air\",\n" +
" \"uuid\": \"abcdefab-1234-1234-abcd-123498761234\",\n" +
" \"configModule\": \"WiFiBTOnboardingNotify_AirPurifier_Core400S_EU\",\n" +
" \"macID\": \"ab:cd:ef:12:34:56\",\n" +
" \"mode\": \"simModeData\",\n" +
" \"speed\": 4,\n" +
" \"extension\": {\n" +
" \"airQuality\": -1,\n" +
" \"airQualityLevel\": 1,\n" +
" \"mode\": \"auto\",\n" +
" \"fanSpeedLevel\": \"1\"\n" +
" },\n" +
" \"currentFirmVersion\": null,\n" +
" \"subDeviceNo\": \"simSubDevice\",\n" +
" \"subDeviceType\": \"simSubDeviceType\",\n" +
" \"deviceFirstSetupTime\": \"Oct 15, 2021 3:43:02 PM\"\n" +
" }\n" +
" ]\n" +
" }\n" +
"}";
@Test
public void testParseManagedDevicesSearchGoodResponse() {
VesyncManagedDevicesPage response = VeSyncConstants.GSON.fromJson(testGoodSearchResponsePageBody,
VesyncManagedDevicesPage.class);
if (response != null) {
assertEquals("1634387642", response.getTraceId());
assertEquals("1", response.result.getPageNo());
assertEquals("100", response.result.getPageSize());
assertEquals("2", response.result.getTotal());
assertEquals("1", String.valueOf(response.result.list.length));
assertEquals("EU", response.result.list[0].getDeviceRegion());
assertEquals("Air Filter", response.result.list[0].getDeviceName());
assertEquals("https://image.vesync.com/defaultImages/Core_400S_Series/icon_core400s_purifier_160.png", response.result.list[0].getDeviceImg());
assertEquals("on", response.result.list[0].getDeviceStatus());
assertEquals("online", response.result.list[0].getConnectionStatus());
assertEquals("WiFi+BTOnboarding+BTNotify", response.result.list[0].getConnectionType());
assertEquals("Core400S", response.result.list[0].getDeviceType());
assertEquals("wifi-air", response.result.list[0].getType());
assertEquals("abcdefab-1234-1234-abcd-123498761234", response.result.list[0].getUuid());
assertEquals("WiFiBTOnboardingNotify_AirPurifier_Core400S_EU", response.result.list[0].getConfigModule());
assertEquals("simModeData",response.result.list[0].getMode());
assertEquals("simSubDevice", response.result.list[0].getSubDeviceNo());
assertEquals("simSubDeviceType", response.result.list[0].getSubDeviceType());
assertEquals( "4", response.result.list[0].getSpeed());
assertEquals("cidValue1",response.result.list[0].getCid());
assertEquals("ab:cd:ef:12:34:56", response.result.list[0].getMacId());
} else {
fail("GSON returned null");
}
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handler.responses;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.responses.VesyncResponse;
/**
* The {@link VesyncResponseTest} class implements unit test case for {@link VesyncResponse}
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VesyncResponseTest {
@Test
public void checkBaseFields() {
String baseTestResponse = "{\"traceId\":\"1234569876\",\r\n\"code\": 142,\r\n\"msg\": \"Response Text\"\r\n}";
VesyncResponse response = VeSyncConstants.GSON.fromJson(baseTestResponse, VesyncResponse.class);
if (response != null) {
assertEquals("1234569876", response.getTraceId());
assertEquals("142", response.getCode());
assertEquals("Response Text", response.msg);
assertEquals(false, response.isMsgSuccess());
} else {
fail("GSON returned null");
}
}
@Test
public void checkResponseSuccessMsg() {
String baseTestResponse = "{\"traceId\":\"1234569876\",\r\n\"code\": 142,\r\n\"msg\": \"request success\"\r\n}";
VesyncResponse response = VeSyncConstants.GSON.fromJson(baseTestResponse, VesyncResponse.class);
if (response != null) {
assertEquals(true, response.isMsgSuccess());
} else {
fail("GSON returned null");
}
}
}

View File

@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vesync.internal.handler.responses.v1;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.vesync.internal.VeSyncConstants;
import org.openhab.binding.vesync.internal.dto.responses.VesyncLoginResponse;
import org.openhab.binding.vesync.internal.dto.responses.v1.VesyncV1AirPurifierDeviceDetailsResponse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
/**
* The {@link VesyncV1AirPurifierDeviceDetailsTest} class implements unit test case for {@link VesyncLoginResponse}
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class VesyncV1AirPurifierDeviceDetailsTest {
public final static String testAirPurifierResponseBasedOnCore400S = "{\n" +
" \"code\": 0,\n" +
" \"msg\": \"request success\",\n" +
" \"traceId\": \"1634255391\",\n" +
" \"screenStatus\": \"on1\",\n" +
" \"airQuality\": 1,\n" +
" \"level\": 2,\n" +
" \"mode\": \"manual\",\n" +
" \"deviceName\": \"Lounge Air Purifier\",\n" +
" \"currentFirmVersion\": \"1.0.17\",\n" +
" \"childLock\": \"off1\",\n" +
" \"deviceStatus\": \"on2\",\n" +
" \"deviceImg\": \"https://image.vesync.com/defaultImages/Core_400S_Series/icon_core400s_purifier_160.png\",\n" +
" \"connectionStatus\": \"online\"\n" +
"}";
@Test
public void testParseV1AirPurifierDeviceDetailsResponse() {
VesyncV1AirPurifierDeviceDetailsResponse response = VeSyncConstants.GSON.fromJson(testAirPurifierResponseBasedOnCore400S,
VesyncV1AirPurifierDeviceDetailsResponse.class);
if (response != null) {
assertEquals("on1", response.getScreenStatus());
assertEquals(1, response.getAirQuality());
assertEquals(2, response.getLevel());
assertEquals("manual", response.getMode());
assertEquals("Lounge Air Purifier", response.getDeviceName());
assertEquals("1.0.17", response.getCurrentFirmVersion());
assertEquals("off1", response.getChildLock());
assertEquals("on2", response.getDeviceStatus());
assertEquals("https://image.vesync.com/defaultImages/Core_400S_Series/icon_core400s_purifier_160.png", response.getDeviceImgUrl());
assertEquals("online", response.getConnectionStatus());
} else {
fail("GSON returned null");
}
}
}

View File

@ -370,6 +370,7 @@
<module>org.openhab.binding.venstarthermostat</module>
<module>org.openhab.binding.ventaair</module>
<module>org.openhab.binding.verisure</module>
<module>org.openhab.binding.vesync</module>
<module>org.openhab.binding.vigicrues</module>
<module>org.openhab.binding.vitotronic</module>
<module>org.openhab.binding.volvooncall</module>