From 4aa47e5fb15665e33a1b207370a53c714626341e Mon Sep 17 00:00:00 2001 From: David Goodyear Date: Sun, 23 Jul 2023 15:49:56 +0100 Subject: [PATCH] [veSync] Vital and 131 Purifiers base support PUR131 and Vital additions Signed-off-by: David Goodyear --- bundles/org.openhab.binding.vesync/README.md | 110 ++++-- .../vesync/internal/VeSyncConstants.java | 4 +- .../internal/api/VeSyncV2ApiHelper.java | 5 +- .../discovery/VeSyncDiscoveryService.java | 3 + .../dto/requests/VeSyncProtocolConstants.java | 7 +- .../internal/dto/requests/VeSyncRequest.java | 5 + .../VeSyncRequestManagedDeviceBypassV2.java | 69 ++++ .../dto/requests/VeSyncRequestV1Command.java | 45 +++ .../VeSyncRequestV1ManagedDeviceDetails.java | 3 +- .../dto/requests/VeSyncRequestV1SetLevel.java | 36 ++ .../dto/requests/VeSyncRequestV1SetMode.java | 36 ++ .../requests/VeSyncRequestV1SetStatus.java | 36 ++ .../VeSyncV2BypassPurifierStatus.java | 3 +- .../VeSyncV2BypassPurifierStatusVital.java | 110 ++++++ ...yncV1AirPurifierDeviceDetailsResponse.java | 23 ++ .../handlers/VeSyncBaseDeviceHandler.java | 29 +- .../VeSyncDeviceAirPurifierHandler.java | 333 ++++++++++++------ .../VeSyncDevicePurifierMetadata.java | 69 ++++ .../resources/OH-INF/i18n/vesync.properties | 5 + .../resources/OH-INF/thing/thing-types.xml | 18 + 20 files changed, 812 insertions(+), 137 deletions(-) create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1Command.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetLevel.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetMode.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetStatus.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatusVital.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDevicePurifierMetadata.java diff --git a/bundles/org.openhab.binding.vesync/README.md b/bundles/org.openhab.binding.vesync/README.md index f41dc8d548d08..638a95298fbb9 100644 --- a/bundles/org.openhab.binding.vesync/README.md +++ b/bundles/org.openhab.binding.vesync/README.md @@ -9,7 +9,7 @@ Air Humidifier models supported are Dual 200S, Classic 300S, 600S, OasisMist Sma ## Awaiting User Verification Models -Air Filtering models supported are Core200S and Core600S. +Air Filtering models supported are Core200S, Core600S, 131S models and the Vital 100S, 200S. Air Humidifier Classic 200S (Same as 300S without the nightlight from initial checks) ## Supported Things @@ -66,25 +66,27 @@ Channel names in **bold** are read/write, everything else is read-only ### AirPurifier Thing -| Channel | Type | Description | Model's Supported | Controllable Values | -|----------------------|----------------------|------------------------------------------------------------|-------------------|-----------------------| -| **enabled** | Switch | Whether the hardware device is enabled (Switched on) | 600S, 400S, 300S | [ON, OFF] | -| **childLock** | Switch | Whether the child lock (display lock is enabled) | 600S, 400S, 300S | [ON, OFF] | -| **display** | Switch | Whether the display is enabled (display is shown) | 600S, 400S, 300S | [ON, OFF] | -| **fanMode** | String | The operation mode of the fan | 600S, 400S | [auto, manual, sleep] | -| **fanMode** | String | The operation mode of the fan | 200S, 300S, | [manual, sleep] | -| **manualFanSpeed** | Number:Dimensionless | The speed of the fan when in manual mode | 600S, 400S | [1...4] | -| **manualFanSpeed** | Number:Dimensionless | The speed of the fan when in manual mode | 300S | [1...3] | -| **nightLightMode** | String | The night lights mode | 200S, 300S | [on, dim, off] | -| filterLifePercentage | Number:Dimensionless | The remaining filter life as a percentage | 600S, 400S, 300S | | -| airQuality | Number:Dimensionless | The air quality as represented by the Core200S / Core300S | 600S, 400S, 300S | | -| airQualityPM25 | Number:Density | The air quality as represented by the Core400S | 600S, 400S, 300S | | -| errorCode | Number:Dimensionless | The error code reported by the device | 600S, 400S, 300S | | -| timerExpiry | DateTime | The expected expiry time of the current timer | 600S, 400S | | -| schedulesCount | Number:Dimensionless | The number schedules configured | 600S, 400S | | -| configDisplayForever | Switch | Config: Whether the display will disable when not active | 600S, 400S, 300S | | -| configAutoMode | String | Config: The mode of operation when auto is active | 600S, 400S, 300S | | -| configAutoRoomSize | Number:Dimensionless | Config: The room size set when auto utilises the room size | 600S, 400S, 300S | | +| Channel | Type | Description | Model's Supported | Controllable Values | +|----------------------|----------------------|------------------------------------------------------------|------------------------------------------------|----------------------------| +| **enabled** | Switch | Whether the hardware device is enabled (Switched on) | 131S, 600S, 400S, 300S, Vital 100S, Vital 200S | [ON, OFF] | +| **childLock** | Switch | Whether the child lock (display lock is enabled) | 600S, 400S, 300S, Vital 100S, Vital 200S | [ON, OFF] | +| **display** | Switch | Whether the display is enabled (display is shown) | 131S, 600S, 400S, 300S, Vital 100S, Vital 200S | [ON, OFF] | +| **fanMode** | String | The operation mode of the fan | 131S, 600S, 400S, Vital 100S | [auto, manual, sleep] | +| **fanMode** | String | The operation mode of the fan | 200S, 300S, | [manual, sleep] | +| **fanMode** | String | The operation mode of the fan | Vital 200S | [auto, manual, sleep, pet] | +| **manualFanSpeed** | Number:Dimensionless | The speed of the fan when in manual mode | 600S, 400S | [1...4] | +| **manualFanSpeed** | Number:Dimensionless | The speed of the fan when in manual mode | 131S, 300S | [1...3] | +| **manualFanSpeed** | Number:Dimensionless | The speed of the fan when in manual mode | Vital 100S,Vital 200S | [1...5] | +| **nightLightMode** | String | The night lights mode | 200S, 300S | [on, dim, off] | +| filterLifePercentage | Number:Dimensionless | The remaining filter life as a percentage | 131S, 600S, 400S, 300S, Vital 100S, Vital 200S | | +| airQuality | Number:Dimensionless | The air quality as represented by the Core200S / Core300S | 131S, 600S, 400S, 300S, Vital 100S, Vital 200S | | +| airQualityPM25 | Number:Density | The air quality as represented by the Core400S | 600S, 400S, 300S, Vital 100S, Vital 200S | | +| 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 @@ -137,7 +139,7 @@ DateTime LoungeAPTimerExpiry "Lounge Air Purifier Timer Ex Number LoungeAPSchedulesCount "Lounge Air Purifier Schedules Count" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:schedulesCount" } ``` -#### Air Purifier Core 200S/300S Model +#### Air Purifier Core 200S / 300S Model ```java Switch LoungeAPPower "Lounge Air Purifier Power" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:enabled" } @@ -155,6 +157,29 @@ DateTime LoungeAPTimerExpiry "Lounge Air Purifier Timer Ex Number LoungeAPSchedulesCount "Lounge Air Purifier Schedules Count" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:schedulesCount" } ``` +#### Air Purifier 131s Models + +```java +Switch LoungeAPPower "Lounge Air Purifier Power" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:enabled" } +Switch LoungeAPDisplay "Lounge Air Purifier Display" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:display" } +Number:Dimensionless LoungeAPFilterRemainingUse "Lounge Air Purifier Filter Remaining [%.0f %unit%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:filterLifePercentage" } +String LoungeAPMode "Lounge Air Purifier Mode [%s]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:fanMode" } +Number:Dimensionless LoungeAPManualFanSpeed "Lounge Air Purifier Manual Fan Speed" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:manualFanSpeed" } +Number:Dimensionless LoungeAPAirQuality "Lounge Air Purifier Air Quality" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:airQuality" } +``` + +#### Air Purifier Vital 100s / 200s Models + +```java +Switch LoungeAPPower "Lounge Air Purifier Power" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:enabled" } +Switch LoungeAPDisplay "Lounge Air Purifier Display" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:display" } +Switch LoungeAPControlsLock "Lounge Air Purifier Controls Locked" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:childLock" } +Number:Dimensionless LoungeAPFilterRemainingUse "Lounge Air Purifier Filter Remaining [%.0f %unit%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:filterLifePercentage" } +String LoungeAPMode "Lounge Air Purifier Mode [%s]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:fanMode" } +Number:Dimensionless LoungeAPManualFanSpeed "Lounge Air Purifier Manual Fan Speed" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:manualFanSpeed" } +Number:Density LoungeAPAirQuality "Lounge Air Purifier Air Quality [%.0f% %unit%]" { channel="vesync:airPurifier:vesyncServers:loungeAirFilter:airQualityPM25" } +``` + #### Air Humidifier Classic 200S / Dual 200S Model ```java @@ -234,7 +259,7 @@ Frame { } ``` -#### Air Purifier Core 200S/300S Model +#### Air Purifier Core 200S / 300S Model ```perl Frame { @@ -251,6 +276,47 @@ Frame { } ``` +#### Air Purifier 131s Models + +```perl +Frame { + Switch item=LoungeAPPower label="Power" + Text item=LoungeAPFilterRemainingUse label="Filter Remaining" + Switch item=LoungeAPDisplay label="Display" + Text item=LoungeAPAirQuality label="Air Quality [%.0f]" + Switch item=LoungeAPMode label="Mode" mappings=[auto="Auto",manual="Manual Fan Control", sleep="Sleeping"] icon="settings" + Switch item=LoungeAPManualFanSpeed label="Manual Fan Speed [%.0f]" mappings=[1="1", 2="2", 3="3"] icon="settings" +} +``` + +#### Air Purifier Vital 100S Models + +```perl +Frame { + Switch item=LoungeAPPower label="Power" + Text item=LoungeAPFilterRemainingUse label="Filter Remaining" + Switch item=LoungeAPDisplay label="Display" + Text item=LoungeAPAirQuality label="Air Quality [%.0f (PM2.5)]" + Switch item=LoungeAPControlsLock label="Controls Locked" + Switch item=LoungeAPMode label="Mode" mappings=[auto="Auto", manual="Manual Fan Control", sleep="Sleeping"] icon="settings" + Switch item=LoungeAPManualFanSpeed label="Manual Fan Speed [%.0f]" mappings=[1="1", 2="2", 3="3", 4="4", 5="5"] icon="settings" +} +``` + +#### Air Purifier Vital 200S Models + +```perl +Frame { + Switch item=LoungeAPPower label="Power" + Text item=LoungeAPFilterRemainingUse label="Filter Remaining" + Switch item=LoungeAPDisplay label="Display" + Text item=LoungeAPAirQuality label="Air Quality [%.0f (PM2.5)]" + Switch item=LoungeAPControlsLock label="Controls Locked" + Switch item=LoungeAPMode label="Mode" mappings=[auto="Auto", manual="Manual Fan Control", sleep="Sleeping", pet="Pet"] icon="settings" + Switch item=LoungeAPManualFanSpeed label="Manual Fan Speed [%.0f]" mappings=[1="1", 2="2", 3="3", 4="4", 5="5"] icon="settings" +} +``` + #### Air Humidifier Classic 200S / Dual 200S Model ```perl diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncConstants.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncConstants.java index 33f5972f77239..95e7890df66d8 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncConstants.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncConstants.java @@ -30,7 +30,7 @@ public class VeSyncConstants { public static final Gson GSON = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting() - .disableHtmlEscaping().serializeNulls().create(); + .disableHtmlEscaping().create(); private static final String BINDING_ID = "vesync"; @@ -65,6 +65,8 @@ public class VeSyncConstants { public static final String DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE = "configAutoRoomSize"; public static final String DEVICE_CHANNEL_AF_SCHEDULES_COUNT = "schedulesCount"; public static final String DEVICE_CHANNEL_AF_NIGHT_LIGHT = "nightLightMode"; + public static final String DEVICE_CHANNEL_AF_LIGHT_DETECTION = "lightDetection"; + public static final String DEVICE_CHANNEL_AF_LIGHT_DETECTED = "lightDetected"; // Humidity related channels public static final String DEVICE_CHANNEL_WATER_LACKS = "waterLacking"; diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/VeSyncV2ApiHelper.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/VeSyncV2ApiHelper.java index bef2421582d1e..77d4309d939cc 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/VeSyncV2ApiHelper.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/VeSyncV2ApiHelper.java @@ -171,12 +171,13 @@ public String reqV1Authorized(final String url, final VeSyncAuthenticatedRequest private String directReqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData) throws AuthenticationException { try { - Request request = httpClient.POST(url); + Request request = httpClient.newRequest(url).method(requestData.httpMethod); // No headers for login request.content(new StringContentProvider(VeSyncConstants.GSON.toJson(requestData))); - logger.debug("POST @ {} with content\r\n{}", url, VeSyncConstants.GSON.toJson(requestData)); + logger.debug("{} @ {} with content\r\n{}", requestData.httpMethod, url, + VeSyncConstants.GSON.toJson(requestData)); request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8"); diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/discovery/VeSyncDiscoveryService.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/discovery/VeSyncDiscoveryService.java index ec0e0c92ef858..3de7a18708bbc 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/discovery/VeSyncDiscoveryService.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/discovery/VeSyncDiscoveryService.java @@ -106,6 +106,9 @@ protected void stopBackgroundDiscovery() { @Override protected void startScan() { + if (bridgeHandler == null) { + return; + } // If the bridge is not online no other thing devices can be found, so no reason to scan at this moment. removeOlderResults(getTimestampOfLastScan()); if (ThingStatus.ONLINE.equals(bridgeHandler.getThing().getStatus())) { diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncProtocolConstants.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncProtocolConstants.java index 0fdc8744ba443..205e1148a2157 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncProtocolConstants.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncProtocolConstants.java @@ -23,6 +23,7 @@ public interface VeSyncProtocolConstants { String MODE_AUTO = "auto"; String MODE_MANUAL = "manual"; String MODE_SLEEP = "sleep"; + String MODE_PET = "pet"; String MODE_ON = "on"; String MODE_DIM = "dim"; @@ -49,12 +50,16 @@ public interface VeSyncProtocolConstants { String DEVICE_SET_NIGHT_LIGHT = "setNightLight"; String DEVICE_GET_PURIFIER_STATUS = "getPurifierStatus"; String DEVICE_LEVEL_TYPE_WIND = "wind"; + String DEVICE_SET_LIGHT_DETECTION = "setLightDetectionSwitch"; /** * Base URL for AUTHENTICATION REQUESTS */ String PROTOCOL = "https"; - String HOST_ENDPOINT = PROTOCOL + "://smartapi.vesync.com/cloud"; + String SERVER_ADDRESS = "smartapi.vesync.com"; + String SERVER_ENDPOINT = PROTOCOL + "://" + SERVER_ADDRESS; + + String HOST_ENDPOINT = SERVER_ENDPOINT + "/cloud"; String V1_LOGIN_ENDPOINT = HOST_ENDPOINT + "/v1/user/login"; String V1_MANAGED_DEVICES_ENDPOINT = HOST_ENDPOINT + "/v1/deviceManaged/devices"; String V2_BYPASS_ENDPOINT = HOST_ENDPOINT + "/v2/deviceManaged/bypassV2"; diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequest.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequest.java index cee7608101846..2d7e5195a2d95 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequest.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequest.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.vesync.internal.dto.requests; +import javax.ws.rs.HttpMethod; + import com.google.gson.annotations.SerializedName; /** @@ -21,6 +23,8 @@ */ public class VeSyncRequest { + public transient String httpMethod; + @SerializedName("timeZone") public String timeZone = "America/New_York"; @@ -44,5 +48,6 @@ public class VeSyncRequest { public VeSyncRequest() { traceId = String.valueOf(System.currentTimeMillis()); + httpMethod = HttpMethod.POST; } } diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestManagedDeviceBypassV2.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestManagedDeviceBypassV2.java index aea75c0e82d3a..d4f56fe4585be 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestManagedDeviceBypassV2.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestManagedDeviceBypassV2.java @@ -55,6 +55,75 @@ public class VesyncManagedDeviceBase { public static class EmptyPayload { } + public static class SetLightDetectionPayload extends EmptyPayload { + + public SetLightDetectionPayload(final boolean enabled) { + lightDetectionSwitch = enabled ? 1 : 0; + } + + @SerializedName("lightDetectionSwitch") + public int lightDetectionSwitch = -1; + } + + public static class SetPowerPayload extends EmptyPayload { + + public SetPowerPayload(final boolean enabled, final int switchIdx) { + this.powerSwitch = enabled ? 1 : 0; + this.switchIdx = switchIdx; + } + + @SerializedName("switchIdx") + public int switchIdx = -1; + + @SerializedName("powerSwitch") + public int powerSwitch = -1; + } + + public static class SetChildLockPayload extends EmptyPayload { + + public SetChildLockPayload(final boolean enabled) { + this.childLockSwitch = enabled ? 1 : 0; + } + + @SerializedName("childLockSwitch") + public int childLockSwitch = -1; + } + + public static class SetScreenSwitchPayload extends EmptyPayload { + + public SetScreenSwitchPayload(final boolean enabled) { + this.screenSwitch = enabled ? 1 : 0; + } + + @SerializedName("screenSwitch") + public int screenSwitch = -1; + } + + public static class SetManualSpeedLevelPayload extends EmptyPayload { + + public SetManualSpeedLevelPayload(final int manualSpeedLevel) { + this.manualSpeedLevel = manualSpeedLevel; + } + + @SerializedName("levelIdx") + public int levelIdx = 0; + + @SerializedName("levelType") + public String levelType = "wind"; + + @SerializedName("manualSpeedLevel") + public int manualSpeedLevel = -1; + } + + public static class SetWorkModePayload extends EmptyPayload { + public SetWorkModePayload(final String workMode) { + this.workMode = workMode; + } + + @SerializedName("workMode") + public String workMode = ""; + } + public static class SetSwitchPayload extends EmptyPayload { public SetSwitchPayload(final boolean enabled, final int id) { diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1Command.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1Command.java new file mode 100644 index 0000000000000..a1452f09695df --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1Command.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import javax.ws.rs.HttpMethod; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncRequestV1Command} is the Java class as a DTO to define the base implementation of a V1 command for + * the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestV1Command extends VeSyncAuthenticatedRequest { + + @SerializedName("uuid") + public String uuid = null; + + public VeSyncRequestV1Command(final String deviceUuid) { + // Exclude fields that shouldn't be there by setting to null + super.phoneOS = null; + super.phoneBrand = null; + super.method = null; + super.appVersion = null; + super.httpMethod = HttpMethod.PUT; + // Set the required payload parameters + uuid = deviceUuid; + } + + public String getUuid() { + return uuid; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1ManagedDeviceDetails.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1ManagedDeviceDetails.java index e5c04095ec36f..8d1b8201e4d0a 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1ManagedDeviceDetails.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1ManagedDeviceDetails.java @@ -18,7 +18,8 @@ import com.google.gson.annotations.SerializedName; /** - * The {@link VeSyncRequestV1ManagedDeviceDetails} is the Java class as a DTO to hold login credentials for the Vesync + * The {@link VeSyncRequestV1ManagedDeviceDetails} is the Java class as a DTO to request the managed device details for + * the Vesync * API. * * @author David Goodyear - Initial contribution diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetLevel.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetLevel.java new file mode 100644 index 0000000000000..ea50568d0cf76 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetLevel.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncRequestV1SetLevel} is the Java class as a DTO define a V1 Set Level command for the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestV1SetLevel extends VeSyncRequestV1Command { + + @SerializedName("level") + public Integer level = null; + + public VeSyncRequestV1SetLevel(final String deviceUuid, final int level) { + super(deviceUuid); + this.level = level; + } + + public Integer getLevel() { + return level; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetMode.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetMode.java new file mode 100644 index 0000000000000..b96f6763d7978 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetMode.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncRequestV1SetMode} is the Java class as a DTO define a V1 Set Mode command for the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestV1SetMode extends VeSyncRequestV1Command { + + @SerializedName("mode") + public String mode = null; + + public VeSyncRequestV1SetMode(final String deviceUuid, final String mode) { + super(deviceUuid); + this.mode = mode; + } + + public String getMode() { + return mode; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetStatus.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetStatus.java new file mode 100644 index 0000000000000..80a1d292be7cd --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestV1SetStatus.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncRequestV1SetStatus} is the Java class as a DTO define a V1 Set Status command for the Vesync + * API. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncRequestV1SetStatus extends VeSyncRequestV1Command { + + @SerializedName("status") + public String status = null; + + public VeSyncRequestV1SetStatus(final String deviceUuid, final String status) { + super(deviceUuid); + this.status = status; + } + + public String getStatus() { + return status; + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatus.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatus.java index 193fd6acae8b0..fb3ab6ad455ce 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatus.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatus.java @@ -16,8 +16,7 @@ /** * The {@link VeSyncV2BypassPurifierStatus} is a Java class used as a DTO to hold the Vesync's API's common response - * data, - * in regards to an Air Purifier device. + * data, in regards to an Air Purifier device. * * @author David Goodyear - Initial contribution */ diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatusVital.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatusVital.java new file mode 100644 index 0000000000000..7d2cd89844b38 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassPurifierStatusVital.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncV2BypassPurifierStatusVital} is a Java class used as a DTO to hold the Vesync's API's common + * response data, in regards to an Vital based Air Purifier device. + * + * @author David Goodyear - Initial contribution + */ +public class VeSyncV2BypassPurifierStatusVital 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("level") + public int level; + + @SerializedName("air_quality") + public int airQuality; + + @SerializedName("display") + public boolean display; + + @SerializedName("powerSwitch") + public int power_switch; + + public boolean getPowerSwitch() { + return power_switch == 1; + } + + @SerializedName("workMode") + public String workMode; + + @SerializedName("fanSpeedLevel") + public int fanSpeedLevel; + + @SerializedName("manualSpeedLevel") + public int manualSpeedLevel; + + @SerializedName("filterLifePercent") + public int filterLifePercent; + + @SerializedName("childLockSwitch") + public int childLockSwitch; + + public boolean getChildLockSwitch() { + return childLockSwitch == 1; + } + + @SerializedName("screenState") + public int screenState; + + public boolean getScreenState() { + return screenState == 1; + } + + @SerializedName("display_forever") + public boolean display_forever; + + @SerializedName("lightDetectionSwitch") + public int lightDetectionSwitch; + + public boolean getLightDetectionSwitch() { + return lightDetectionSwitch == 1; + } + + @SerializedName("environmentLightState") + public int environmentLightState; + + public boolean getEnvironmentLightState() { + return environmentLightState == 1; + } + + @SerializedName("screenSwitch") + public int screenSwitch; + + public boolean getScreenSwitch() { + return screenSwitch == 1; + } + + @SerializedName("PM25") + public int PM25; + + @SerializedName("timerRemain") + public String timerRemain; + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/v1/VeSyncV1AirPurifierDeviceDetailsResponse.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/v1/VeSyncV1AirPurifierDeviceDetailsResponse.java index 356f31adc663d..20e248499870a 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/v1/VeSyncV1AirPurifierDeviceDetailsResponse.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/v1/VeSyncV1AirPurifierDeviceDetailsResponse.java @@ -53,6 +53,29 @@ public String getMode() { return mode; } + @SerializedName("activeTime") + public int activeTime; + + public int getActiveTime() { + return activeTime; + } + + @SerializedName("filterLife") + public FilterLife filter; + + public int getFilterPercent() { + return filter.getPercent(); + } + + public class FilterLife { + @SerializedName("percent") + public int percent; + + public int getPercent() { + return percent; + } + } + @SerializedName("deviceName") public String deviceName; diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBaseDeviceHandler.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBaseDeviceHandler.java index 260a7df858896..13c2eeb976ee0 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBaseDeviceHandler.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBaseDeviceHandler.java @@ -32,6 +32,7 @@ import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration; import org.openhab.binding.vesync.internal.VeSyncDeviceConfiguration; import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants; import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2; import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase; import org.openhab.binding.vesync.internal.exceptions.AuthenticationException; @@ -47,6 +48,7 @@ import org.openhab.core.thing.binding.BridgeHandler; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.types.State; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -415,7 +417,20 @@ protected final String sendV2BypassControlCommand(final String method, return result; } - public final String sendV1Command(final String method, final String url, final VeSyncAuthenticatedRequest request) { + protected final String sendV1ControlCommand(final String urlPath, final VeSyncAuthenticatedRequest request) { + return sendV1ControlCommand(urlPath, request, true); + } + + protected final String sendV1ControlCommand(final String urlPath, final VeSyncAuthenticatedRequest request, + final boolean readbackDevice) { + final String result = sendV1Command(urlPath, request); + if (!result.equals(EMPTY_STRING) && readbackDevice) { + performReadbackPoll(); + } + return result; + } + + public final String sendV1Command(final String urlPath, final VeSyncAuthenticatedRequest request) { if (ThingStatus.OFFLINE.equals(this.thing.getStatus())) { logger.debug("Command blocked as device is offline"); return EMPTY_STRING; @@ -427,6 +442,7 @@ public final String sendV1Command(final String method, final String url, final V } VeSyncClient client = getVeSyncClient(); if (client != null) { + final String url = VeSyncProtocolConstants.SERVER_ENDPOINT + "/" + urlPath; return client.reqV2Authorized(url, deviceLookupKey, request); } else { throw new DeviceUnknownException("Missing client"); @@ -552,4 +568,15 @@ public static VeSyncDeviceMetadata getDeviceFamilyMetadata(final @Nullable Strin public VeSyncDeviceMetadata getDeviceFamilyMetadata(final @Nullable String deviceType) { return getDeviceFamilyMetadata(deviceType, getDeviceFamilyProtocolPrefix(), getSupportedDeviceMetadata()); } + + @Override + protected void updateState(final String channelID, final State state) { + // In case of any unexpected decoding issues log them, so that the necessary adjustments can + // be done. (Not expected but just in case). + try { + super.updateState(channelID, state); + } catch (final Exception e) { + logger.warn("Please report issue - could not update channel {} with error {}", channelID, e.toString()); + } + } } diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceAirPurifierHandler.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceAirPurifierHandler.java index ab6d8ac8c0486..aa6026e979ad2 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceAirPurifierHandler.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceAirPurifierHandler.java @@ -19,8 +19,11 @@ import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.validation.constraints.NotNull; @@ -29,7 +32,12 @@ import org.openhab.binding.vesync.internal.VeSyncConstants; import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2; import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestV1ManagedDeviceDetails; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestV1SetLevel; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestV1SetMode; +import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestV1SetStatus; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncResponse; import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassPurifierStatus; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassPurifierStatusVital; import org.openhab.binding.vesync.internal.dto.responses.v1.VeSyncV1AirPurifierDeviceDetailsResponse; import org.openhab.core.cache.ExpiringCache; import org.openhab.core.library.items.DateTimeItem; @@ -55,6 +63,7 @@ * @author David Goodyear - Initial contribution */ @NonNullByDefault +@SuppressWarnings("serial") public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler { public static final String DEV_TYPE_FAMILY_AIR_PURIFIER = "LAP"; @@ -68,27 +77,51 @@ public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler { public static final String DEV_FAMILY_PUR_131S = "131S"; - public static final VeSyncDeviceMetadata CORE200S = new VeSyncDeviceMetadata(DEV_FAMILY_CORE_200S, - Arrays.asList("C201S", "C202S"), List.of("Core200S")); + public static final String DEV_FAMILY_VITAL_100S = "V100S"; - public static final VeSyncDeviceMetadata CORE300S = new VeSyncDeviceMetadata(DEV_FAMILY_CORE_300S, - List.of("C301S", "C302S"), List.of("Core300S")); + public static final String DEV_FAMILY_VITAL_200S = "V200S"; - public static final VeSyncDeviceMetadata CORE400S = new VeSyncDeviceMetadata(DEV_FAMILY_CORE_400S, List.of("C401S"), - List.of("Core400S")); + private static final List FAN_MODES_WITH_PET = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP, MODE_PET); - public static final VeSyncDeviceMetadata CORE600S = new VeSyncDeviceMetadata(DEV_FAMILY_CORE_600S, List.of("C601S"), - List.of("Core600S")); + private static final List FAN_MODES_NO_PET = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP); + private static final List FAN_MODES_MAN_SLEEP = Arrays.asList(MODE_MANUAL, MODE_SLEEP); + private static final List NIGHT_LIGHTS = Arrays.asList(MODE_ON, MODE_DIM, MODE_OFF); - public static final VeSyncDeviceMetadata PUR131S = new VeSyncDeviceMetadata(DEV_FAMILY_PUR_131S, - Collections.emptyList(), Arrays.asList("LV-PUR131S", "LV-RH131S")); + private static final List NO_NIGHT_LIGHTS = Collections.emptyList(); + public static final VeSyncDevicePurifierMetadata CORE200S = new VeSyncDevicePurifierMetadata(DEV_FAMILY_CORE_200S, + Arrays.asList("C201S", "C202S"), List.of("Core200S"), FAN_MODES_MAN_SLEEP, 1, 3, NIGHT_LIGHTS); - public static final List SUPPORTED_MODEL_FAMILIES = Arrays.asList(CORE600S, CORE400S, - CORE300S, CORE200S, PUR131S); + public static final VeSyncDevicePurifierMetadata CORE300S = new VeSyncDevicePurifierMetadata(DEV_FAMILY_CORE_300S, + List.of("C301S", "C302S"), List.of("Core300S"), FAN_MODES_MAN_SLEEP, 1, 3, NIGHT_LIGHTS); - private static final List CORE_400S600S_FAN_MODES = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP); - private static final List CORE_200S300S_FAN_MODES = Arrays.asList(MODE_MANUAL, MODE_SLEEP); - private static final List CORE_200S300S_NIGHT_LIGHT_MODES = Arrays.asList(MODE_ON, MODE_DIM, MODE_OFF); + public static final VeSyncDevicePurifierMetadata CORE400S = new VeSyncDevicePurifierMetadata(DEV_FAMILY_CORE_400S, + List.of("C401S"), List.of("Core400S"), FAN_MODES_NO_PET, 1, 4, NO_NIGHT_LIGHTS); + + public static final VeSyncDevicePurifierMetadata CORE600S = new VeSyncDevicePurifierMetadata(DEV_FAMILY_CORE_600S, + List.of("C601S"), List.of("Core600S"), FAN_MODES_NO_PET, 1, 4, NO_NIGHT_LIGHTS); + + public static final VeSyncDevicePurifierMetadata VITAL100S = new VeSyncDevicePurifierMetadata(DEV_FAMILY_VITAL_100S, + List.of("V102S"), Collections.emptyList(), FAN_MODES_NO_PET, 1, 5, NO_NIGHT_LIGHTS); + + public static final VeSyncDevicePurifierMetadata VITAL200S = new VeSyncDevicePurifierMetadata(DEV_FAMILY_VITAL_200S, + List.of("V201S"), Collections.emptyList(), FAN_MODES_WITH_PET, 1, 5, NO_NIGHT_LIGHTS); + + public static final VeSyncDevicePurifierMetadata PUR131S = new VeSyncDevicePurifierMetadata(DEV_FAMILY_PUR_131S, + Collections.emptyList(), Arrays.asList("LV-PUR131S", "LV-RH131S"), FAN_MODES_NO_PET, 1, 3, NO_NIGHT_LIGHTS); + + public static final Map DEV_FAMILY_HUMIDIFER_MAP = new HashMap() { + { + put(PUR131S.deviceFamilyName, PUR131S); + put(CORE200S.deviceFamilyName, CORE200S); + put(CORE300S.deviceFamilyName, CORE300S); + put(CORE400S.deviceFamilyName, CORE400S); + put(CORE600S.deviceFamilyName, CORE600S); + put(VITAL100S.deviceFamilyName, VITAL100S); + put(VITAL200S.deviceFamilyName, VITAL200S); + } + }; + public static final List SUPPORTED_MODEL_FAMILIES = DEV_FAMILY_HUMIDIFER_MAP.values().stream() + .collect(Collectors.toList()); private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirPurifierHandler.class); @@ -114,16 +147,28 @@ public void initialize() { switch (deviceFamily) { case DEV_FAMILY_CORE_600S: case DEV_FAMILY_CORE_400S: - toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT }; + toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT, DEVICE_CHANNEL_AF_LIGHT_DETECTION, + DEVICE_CHANNEL_AF_LIGHT_DETECTED }; break; case DEV_FAMILY_PUR_131S: toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT, DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE, DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF, DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, - DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING, DEVICE_CHANNEL_AIRQUALITY_PM25, - DEVICE_CHANNEL_AF_SCHEDULES_COUNT, DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER }; + DEVICE_CHANNEL_AIRQUALITY_PM25, DEVICE_CHANNEL_AF_SCHEDULES_COUNT, + DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER, DEVICE_CHANNEL_ERROR_CODE, + DEVICE_CHANNEL_CHILD_LOCK_ENABLED, DEVICE_CHANNEL_AF_LIGHT_DETECTION, + DEVICE_CHANNEL_AF_LIGHT_DETECTED }; + break; + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + toRemove = new String[] { DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, DEVICE_CHANNEL_AF_SCHEDULES_COUNT, + DEVICE_CHANNEL_AF_NIGHT_LIGHT, DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF, + DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER, DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE, + DEVICE_CHANNEL_AF_LIGHT_DETECTION, DEVICE_CHANNEL_AF_LIGHT_DETECTED, + DEVICE_CHANNEL_ERROR_CODE }; break; default: - toRemove = new String[] { DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, DEVICE_CHANNEL_AF_SCHEDULES_COUNT }; + toRemove = new String[] { DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, DEVICE_CHANNEL_AF_SCHEDULES_COUNT, + DEVICE_CHANNEL_AF_LIGHT_DETECTION, DEVICE_CHANNEL_AF_LIGHT_DETECTED }; } } return toRemove; @@ -164,111 +209,153 @@ public void handleCommand(final ChannelUID channelUID, final Command command) { if (deviceFamily == null) { return; } + final String deviceUuid = getThing().getProperties().get(DEVICE_PROP_DEVICE_UUID); + if (deviceUuid == null) { + return; + } + final VeSyncDevicePurifierMetadata devContraints = DEV_FAMILY_HUMIDIFER_MAP.get(deviceFamily); + if (devContraints == null) { + logger.warn("Could not find device family for {} during handleCommand", deviceFamily); + return; + } scheduler.submit(() -> { if (command instanceof OnOffType) { switch (channelUID.getId()) { case DEVICE_CHANNEL_ENABLED: - sendV2BypassControlCommand(DEVICE_SET_SWITCH, - new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload(command.equals(OnOffType.ON), - 0)); + switch (deviceFamily) { + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + sendV2BypassControlCommand(DEVICE_SET_SWITCH, + new VeSyncRequestManagedDeviceBypassV2.SetPowerPayload( + command.equals(OnOffType.ON), 0)); + break; + case DEV_FAMILY_PUR_131S: + sendV1ControlCommand("131airPurifier/v1/device/deviceStatus", + new VeSyncRequestV1SetStatus(deviceUuid, + command.equals(OnOffType.ON) ? "on" : "off")); + break; + default: + sendV2BypassControlCommand(DEVICE_SET_SWITCH, + new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload( + command.equals(OnOffType.ON), 0)); + } break; case DEVICE_CHANNEL_DISPLAY_ENABLED: - sendV2BypassControlCommand(DEVICE_SET_DISPLAY, - new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON))); + switch (deviceFamily) { + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + sendV2BypassControlCommand(DEVICE_SET_DISPLAY, + new VeSyncRequestManagedDeviceBypassV2.SetScreenSwitchPayload( + command.equals(OnOffType.ON))); + break; + case DEV_FAMILY_PUR_131S: + sendV1ControlCommand("131airPurifier/v1/device/updateScreen", + new VeSyncRequestV1SetStatus(deviceUuid, + command.equals(OnOffType.ON) ? "on" : "off")); + break; + default: + sendV2BypassControlCommand(DEVICE_SET_DISPLAY, + new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON))); + + break; + } break; case DEVICE_CHANNEL_CHILD_LOCK_ENABLED: - sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK, - new VeSyncRequestManagedDeviceBypassV2.SetChildLock(command.equals(OnOffType.ON))); + switch (deviceFamily) { + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK, + new VeSyncRequestManagedDeviceBypassV2.SetChildLockPayload( + command.equals(OnOffType.ON))); + break; + default: + sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK, + new VeSyncRequestManagedDeviceBypassV2.SetChildLock( + command.equals(OnOffType.ON))); + break; + } + break; + case DEVICE_CHANNEL_AF_LIGHT_DETECTION: + sendV2BypassControlCommand(DEVICE_SET_LIGHT_DETECTION, + new VeSyncRequestManagedDeviceBypassV2.SetLightDetectionPayload( + command.equals(OnOffType.ON))); break; } } else if (command instanceof StringType) { switch (channelUID.getId()) { case DEVICE_CHANNEL_FAN_MODE_ENABLED: final String targetFanMode = command.toString().toLowerCase(); + + if (!devContraints.isFanModeSupported(targetFanMode)) { + logger.warn("Fan mode command for \"{}\" is not valid in the ({}) API possible options {}", + command, devContraints.deviceFamilyName, String.join(",", devContraints.fanModes)); + pollForUpdate(); + return; + } switch (deviceFamily) { - case DEV_FAMILY_CORE_600S: - case DEV_FAMILY_CORE_400S: - if (!CORE_400S600S_FAN_MODES.contains(targetFanMode)) { - logger.warn( - "Fan mode command for \"{}\" is not valid in the (Core400S) API possible options {}", - command, String.join(",", CORE_400S600S_FAN_MODES)); - return; - } + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetWorkModePayload(targetFanMode)); break; - case DEV_FAMILY_CORE_200S: - case DEV_FAMILY_CORE_300S: - if (!CORE_200S300S_FAN_MODES.contains(targetFanMode)) { - logger.warn( - "Fan mode command for \"{}\" is not valid in the (Core200S/Core300S) API possible options {}", - command, String.join(",", CORE_200S300S_FAN_MODES)); - return; - } + case DEV_FAMILY_PUR_131S: + sendV1ControlCommand("131airPurifier/v1/device/updateMode", + new VeSyncRequestV1SetMode(deviceUuid, targetFanMode)); break; + default: + sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetMode(targetFanMode)); } - - sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, - new VeSyncRequestManagedDeviceBypassV2.SetMode(targetFanMode)); break; case DEVICE_CHANNEL_AF_NIGHT_LIGHT: final String targetNightLightMode = command.toString().toLowerCase(); - switch (deviceFamily) { - case DEV_FAMILY_CORE_600S: - case DEV_FAMILY_CORE_400S: - logger.warn("Core400S API does not support night light"); - return; - case DEV_FAMILY_CORE_200S: - case DEV_FAMILY_CORE_300S: - if (!CORE_200S300S_NIGHT_LIGHT_MODES.contains(targetNightLightMode)) { - logger.warn( - "Night light mode command for \"{}\" is not valid in the (Core200S/Core300S) API possible options {}", - command, String.join(",", CORE_200S300S_NIGHT_LIGHT_MODES)); - return; - } - - sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT, - new VeSyncRequestManagedDeviceBypassV2.SetNightLight(targetNightLightMode)); - - break; + if (!devContraints.isNightLightModeSupported(targetNightLightMode)) { + logger.warn( + "Night light mode command for \"{}\" is not valid in the ({}) API possible options {}", + command, devContraints.deviceFamilyName, + String.join(",", devContraints.nightLightModes)); + pollForUpdate(); + return; } + sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT, + new VeSyncRequestManagedDeviceBypassV2.SetNightLight(targetNightLightMode)); break; } } else if (command instanceof QuantityType) { switch (channelUID.getId()) { case DEVICE_CHANNEL_FAN_SPEED_ENABLED: - // If the fan speed is being set enforce manual mode - sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, - new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false); - int requestedLevel = ((QuantityType) command).intValue(); - if (requestedLevel < 1) { - logger.warn("Fan speed command less than 1 - adjusting to 1 as the valid API value"); - requestedLevel = 1; + if (!devContraints.isFanSpeedSupported(requestedLevel)) { + logger.warn("Fan speed command for \"{}\" is not valid ({}) API possible options {} -> {}", + command, devContraints.deviceFamilyName, String.valueOf(devContraints.minFanSpeed), + String.valueOf(devContraints.maxFanSpeed)); + pollForUpdate(); + return; } - switch (deviceFamily) { - case DEV_FAMILY_CORE_600S: - case DEV_FAMILY_CORE_400S: - if (requestedLevel > 4) { - logger.warn( - "Fan speed command greater than 4 - adjusting to 4 as the valid (Core400S) API value"); - requestedLevel = 4; - } + case DEV_FAMILY_VITAL_100S: + case DEV_FAMILY_VITAL_200S: + sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetWorkModePayload(MODE_MANUAL)); + sendV2BypassControlCommand(DEVICE_SET_LEVEL, + new VeSyncRequestManagedDeviceBypassV2.SetManualSpeedLevelPayload( + requestedLevel)); break; - case DEV_FAMILY_CORE_200S: - case DEV_FAMILY_CORE_300S: - if (requestedLevel > 3) { - logger.warn( - "Fan speed command greater than 3 - adjusting to 3 as the valid (Core200S/Core300S) API value"); - requestedLevel = 3; - } + case DEV_FAMILY_PUR_131S: + sendV1ControlCommand("131airPurifier/v1/device/updateMode", + new VeSyncRequestV1SetMode(deviceUuid, MODE_MANUAL), false); + sendV1ControlCommand("131airPurifier/v1/device/updateSpeed", + new VeSyncRequestV1SetLevel(deviceUuid, requestedLevel)); break; + default: + sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE, + new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false); + sendV2BypassControlCommand(DEVICE_SET_LEVEL, + new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, + DEVICE_LEVEL_TYPE_WIND, requestedLevel)); } - - sendV2BypassControlCommand(DEVICE_SET_LEVEL, - new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_WIND, - requestedLevel)); break; } } else if (command instanceof RefreshType) { @@ -285,17 +372,10 @@ protected void pollForDeviceData(final ExpiringCache cachedResponse) { if (deviceFamily == null) { return; } - - switch (deviceFamily) { - case DEV_FAMILY_CORE_600S: - case DEV_FAMILY_CORE_400S: - case DEV_FAMILY_CORE_300S: - case DEV_FAMILY_CORE_200S: - processV2BypassPoll(cachedResponse); - break; - case DEV_FAMILY_PUR_131S: - processV1AirPurifierPoll(cachedResponse); - break; + if (!DEV_FAMILY_PUR_131S.equals(deviceFamily)) { + processV2BypassPoll(cachedResponse); + } else { + processV1AirPurifierPoll(cachedResponse); } } @@ -312,7 +392,7 @@ private void processV1AirPurifierPoll(final ExpiringCache cachedResponse boolean cachedDataUsed = response != null; if (response == null) { logger.trace("Requesting fresh response"); - response = sendV1Command("POST", "https://smartapi.vesync.com/131airPurifier/v1/device/deviceDetail", + response = sendV1Command("131airPurifier/v1/device/deviceDetail", new VeSyncRequestV1ManagedDeviceDetails(deviceUuid)); } else { logger.trace("Using cached response {}", response); @@ -353,11 +433,19 @@ private void processV1AirPurifierPoll(final ExpiringCache cachedResponse updateState(DEVICE_CHANNEL_FAN_SPEED_ENABLED, new DecimalType(String.valueOf(purifierStatus.getLevel()))); updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(MODE_ON.equals(purifierStatus.getScreenStatus()))); updateState(DEVICE_CHANNEL_AIRQUALITY_BASIC, new DecimalType(purifierStatus.getAirQuality())); + updateState(DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING, + new QuantityType<>(purifierStatus.filter.getPercent(), Units.PERCENT)); } private void processV2BypassPoll(final ExpiringCache cachedResponse) { + final String deviceFamily = getThing().getProperties().get(DEVICE_PROP_DEVICE_FAMILY); + + final boolean processingVital = DEV_FAMILY_VITAL_100S.equals(deviceFamily) + || DEV_FAMILY_VITAL_200S.equals(deviceFamily); + String response; - VeSyncV2BypassPurifierStatus purifierStatus; + VeSyncResponse purifierStatus = null; + synchronized (pollLock) { response = cachedResponse.getValue(); boolean cachedDataUsed = response != null; @@ -372,8 +460,11 @@ private void processV2BypassPoll(final ExpiringCache cachedResponse) { if (response.equals(EMPTY_STRING)) { return; } - - purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassPurifierStatus.class); + if (processingVital) { + purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassPurifierStatusVital.class); + } else { + purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassPurifierStatus.class); + } if (purifierStatus == null) { return; @@ -393,6 +484,14 @@ private void processV2BypassPoll(final ExpiringCache cachedResponse) { updateStatus(ThingStatus.ONLINE); } + if (processingVital) { + parseV2PollVital((VeSyncV2BypassPurifierStatusVital) purifierStatus); + } else { + parseV2PollNonVital((VeSyncV2BypassPurifierStatus) purifierStatus); + } + } + + private void parseV2PollNonVital(final VeSyncV2BypassPurifierStatus purifierStatus) { if (!"0".equals(purifierStatus.result.getCode())) { logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier"); return; @@ -409,13 +508,10 @@ private void processV2BypassPoll(final ExpiringCache cachedResponse) { updateState(DEVICE_CHANNEL_AIRQUALITY_BASIC, new DecimalType(purifierStatus.result.result.airQuality)); updateState(DEVICE_CHANNEL_AIRQUALITY_PM25, new QuantityType<>(purifierStatus.result.result.airQualityValue, Units.MICROGRAM_PER_CUBICMETRE)); - updateState(DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER, OnOffType.from(purifierStatus.result.result.configuration.displayForever)); - updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF, new StringType(purifierStatus.result.result.configuration.autoPreference.autoType)); - updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE, new DecimalType(purifierStatus.result.result.configuration.autoPreference.roomSize)); @@ -436,4 +532,27 @@ private void processV2BypassPoll(final ExpiringCache cachedResponse) { updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new DecimalType(purifierStatus.result.result.nightLight)); } } + + private void parseV2PollVital(final VeSyncV2BypassPurifierStatusVital purifierStatus) { + if (!"0".equals(purifierStatus.result.getCode())) { + logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier"); + return; + } + + updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(purifierStatus.result.result.getPowerSwitch())); + updateState(DEVICE_CHANNEL_CHILD_LOCK_ENABLED, + OnOffType.from(purifierStatus.result.result.getChildLockSwitch())); + updateState(DEVICE_CHANNEL_AIRQUALITY_BASIC, new DecimalType(purifierStatus.result.result.airQuality)); + updateState(DEVICE_CHANNEL_AIRQUALITY_PM25, + new QuantityType<>(purifierStatus.result.result.PM25, Units.MICROGRAM_PER_CUBICMETRE)); + updateState(DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING, + new QuantityType<>(purifierStatus.result.result.filterLifePercent, Units.PERCENT)); + updateState(DEVICE_CHANNEL_AF_LIGHT_DETECTION, + OnOffType.from(purifierStatus.result.result.getLightDetectionSwitch())); + updateState(DEVICE_CHANNEL_AF_LIGHT_DETECTED, + OnOffType.from(purifierStatus.result.result.getEnvironmentLightState())); + updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(purifierStatus.result.result.getScreenState())); + updateState(DEVICE_CHANNEL_FAN_MODE_ENABLED, new StringType(purifierStatus.result.result.workMode)); + updateState(DEVICE_CHANNEL_FAN_SPEED_ENABLED, new DecimalType(purifierStatus.result.result.fanSpeedLevel)); + } } diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDevicePurifierMetadata.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDevicePurifierMetadata.java new file mode 100644 index 0000000000000..a5d77c2427e6f --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDevicePurifierMetadata.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handlers; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link VeSyncDevicePurifierMetadata} class contains the definition for the control of humidifer device types. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class VeSyncDevicePurifierMetadata extends VeSyncDeviceMetadata { + + // TODO: Add night light supported modes + public VeSyncDevicePurifierMetadata(final String deviceFamilyName, final List deviceGenerations, + final List nonStandardIds, final List fanModes, final int minFanSpeed, + final int maxFanSpeed, final List nightLightModes) { + super(deviceFamilyName, deviceGenerations, nonStandardIds); + this.fanModes = fanModes; + this.minFanSpeed = minFanSpeed; + this.maxFanSpeed = maxFanSpeed; + this.nightLightModes = nightLightModes; + } + + /** + * The fan modes supported by this generation of device + */ + public final List fanModes; + + /** + * The minimum fan speed supported + */ + public final int minFanSpeed; + + /** + * The maximum fan speed supported + */ + public final int maxFanSpeed; + + /** + * The night light supported by this generation of device + */ + public final List nightLightModes; + + public final boolean isFanModeSupported(final String fanMode) { + return fanModes.contains(fanMode); + } + + public final boolean isFanSpeedSupported(final int speed) { + return speed >= minFanSpeed && speed <= maxFanSpeed; + } + + public final boolean isNightLightModeSupported(final String nightLightMode) { + return nightLightModes.contains(nightLightMode); + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/i18n/vesync.properties b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/i18n/vesync.properties index 240a2cf0e09a8..466c8930f680c 100644 --- a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/i18n/vesync.properties +++ b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/i18n/vesync.properties @@ -45,6 +45,7 @@ channel-type.vesync.airPurifierModeType.description = The operating mode the air channel-type.vesync.airPurifierModeType.state.option.auto = Auto channel-type.vesync.airPurifierModeType.state.option.manual = Manual Fan Control channel-type.vesync.airPurifierModeType.state.option.sleep = Sleeping Auto +channel-type.vesync.airPurifierModeType.state.option.pet = Pet Auto channel-type.vesync.airQualityPM25.label = Air Quality PPM2.5 channel-type.vesync.airQualityPM25.description = Indicator of current air quality channel-type.vesync.deviceAFConfigAutoPrefRoomSizeType.label = Config: Room size @@ -66,6 +67,10 @@ channel-type.vesync.deviceAFNightLight.state.option.off = Off channel-type.vesync.deviceAFTimerExpiry.label = Auto Off Expiry channel-type.vesync.deviceAFTimerExpiry.description = The time when the auto off timer will be reached channel-type.vesync.deviceAFTimerExpiry.state.pattern = %1$tF %1$tR +channel-type.vesync.deviceAFLightDetection.label = Light Detection +channel-type.vesync.deviceAFLightDetection.description = If the devices light detection is enabled +channel-type.vesync.deviceAFLightDetected.label = Light Detected +channel-type.vesync.deviceAFLightDetected.description = Indicator if the device detects light channel-type.vesync.deviceAirQualityBasicType.label = Air Quality channel-type.vesync.deviceAirQualityBasicType.description = System representation of air quality channel-type.vesync.deviceAutomaticStopReachTargetType.label = Stop @ Set Point diff --git a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/thing/thing-types.xml index e1ffdac5c496e..5af5565553705 100644 --- a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/thing/thing-types.xml @@ -64,6 +64,9 @@ + + + @@ -169,6 +172,7 @@ + @@ -221,6 +225,20 @@ Configuration: If the devices display is enabled forever + + Switch + + If the devices light detection is enabled + + + + + Switch + + Indicator if the device detects light + + + String