From 65664f2e2edb52b0a7eebdbbc7f8fe0eb6f81543 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:40:13 +0100 Subject: [PATCH] [VeSync] Add support for wifi outlets (#17844) * Add support for wifi outlets Signed-off-by: Marcel Goerentz --- bundles/org.openhab.binding.vesync/README.md | 17 ++ .../internal/VeSyncBridgeConfiguration.java | 3 + .../vesync/internal/VeSyncConstants.java | 14 ++ .../vesync/internal/VeSyncHandlerFactory.java | 5 +- .../internal/api/VeSyncV2ApiHelper.java | 3 +- .../discovery/VeSyncDiscoveryService.java | 20 ++ .../dto/requests/VeSyncProtocolConstants.java | 4 + .../VeSyncRequestGetOutletStatus.java | 73 ++++++ .../VeSyncRequestManagedDeviceBypassV2.java | 17 ++ .../VeSyncV2BypassEnergyHistory.java | 54 +++++ .../responses/VeSyncV2BypassOutletStatus.java | 67 ++++++ .../handlers/VeSyncBridgeHandler.java | 12 +- .../handlers/VeSyncDeviceOutletHandler.java | 217 ++++++++++++++++++ .../resources/OH-INF/i18n/vesync.properties | 5 +- .../resources/OH-INF/thing/thing-types.xml | 108 +++++++++ 15 files changed, 614 insertions(+), 5 deletions(-) create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestGetOutletStatus.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassEnergyHistory.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassOutletStatus.java create mode 100644 bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceOutletHandler.java diff --git a/bundles/org.openhab.binding.vesync/README.md b/bundles/org.openhab.binding.vesync/README.md index 8fdab22420b..4029aab9f1c 100644 --- a/bundles/org.openhab.binding.vesync/README.md +++ b/bundles/org.openhab.binding.vesync/README.md @@ -21,6 +21,7 @@ This binding supports the follow thing types: | Bridge | Bridge | bridge | Manual | A single connection to the VeSync API | | Air Purifier | Thing | airPurifier | Automatic | An Air Purifier supporting V2 e.g. Core200S/Core300S or Core400S unit | | Air Humidifier | Thing | airHumidifier | Automatic | An Air Humidifier supporting V2 e.g. Classic300S or 600s | +| Outlet | Thing | outlet | Automatic | An Outlet supporting V2 eg WHOGPLUG | This binding was developed from the great work in the listed projects. @@ -40,6 +41,7 @@ Once the bridge is configured auto discovery will discover supported devices fro | 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 | +| outletPollInterval | Number | The poll interval (seconds) for outlets | 60 | | backgroundDeviceDiscovery | Switch | Should the system scan periodically for new devices | ON | | refreshBackgroundDeviceDiscovery | Number | Frequency (seconds) of scans for new new devices | 120 | @@ -111,6 +113,21 @@ Channel names in **bold** are read/write, everything else is read-only | timerExpiry | DateTime | The expected expiry time of the current timer | OasisMist1000 | | | | schedulesCount | Number:Dimensionless | The number schedules configured | OasisMist1000 | | one | +### Outlet Thing + +| Channel | Type | Description | Model's Supported | Controllable Values | +|-----------------|------------------------|------------------------------------------------------|-------------------|---------------------| +| **enabled** | Switch | Whether the hardware device is enabled (Switched on) | WHOGPLUG | [ON, OFF] | +| current | Number:ElectricCurrent | Actual current in A | WHOGPLUG | | +| energy | Number:Energy | Today's energy in kWh | WHOGPLUG | | +| power | Number:Power | Current power in W | WHOGPLUG | | +| voltage | ElectricPotential | Current Voltage | WHOGPLUG | | +| highestVoltage | ElectricPotential | Highest Voltage ever measured by the outlet | WHOGPLUG | | +| energyWeek | Number:Energy | Total energy of week in kWh | WHOGPLUG | | +| energyMonth | Number:Energy | Total energy of month in kWh | WHOGPLUG | | +| energyYear | Number:Energy | Total energy of year in kWh | WHOGPLUG | | + + ## Full Example ### Configuration (*.things) diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncBridgeConfiguration.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncBridgeConfiguration.java index 7d7282e1c46..84c883a6003 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncBridgeConfiguration.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncBridgeConfiguration.java @@ -40,4 +40,7 @@ public class VeSyncBridgeConfiguration { */ @Nullable public Integer airPurifierPollInterval; + + @Nullable + public Integer outletPollInterval; } 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 48850f4444a..ba5ad1c65f8 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 @@ -24,6 +24,7 @@ import com.google.gson.GsonBuilder; * used across the whole binding. * * @author David Goodyear - Initial contribution + * @author Marcel Goerentz - Add constants for outlets */ @NonNullByDefault public class VeSyncConstants { @@ -36,11 +37,13 @@ public class VeSyncConstants { public static final long DEFAULT_REFRESH_INTERVAL_DISCOVERED_DEVICES = 3600; public static final long DEFAULT_POLL_INTERVAL_AIR_FILTERS_DEVICES = 10; + public static final long SECONDS_IN_MONTH = 2592000; // 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"); + public static final ThingTypeUID THING_TYPE_OUTLET = new ThingTypeUID(BINDING_ID, "outlet"); // Thing configuration properties public static final String DEVICE_MAC_ID = "macAddress"; @@ -68,6 +71,17 @@ public class VeSyncConstants { public static final String DEVICE_CHANNEL_AF_LIGHT_DETECTION = "lightDetection"; public static final String DEVICE_CHANNEL_AF_LIGHT_DETECTED = "lightDetected"; + // Energy Related Channel Names + public static final String DEVICE_CHANNEL_CURRENT = "current"; + public static final String DEVICE_CHANNEL_ENERGY = "energy"; + public static final String DEVICE_CHANNEL_POWER = "power"; + public static final String DEVICE_CHANNEL_VOLTAGE = "voltage"; + public static final String DEVICE_CHANNEL_VOLTAGE_PT_STATUS = "voltagePTStatus"; + public static final String DEVICE_CHANNEL_HIGHEST_VOLTAGE = "highestVoltage"; + public static final String DEVICE_CHANNEL_ENERGY_WEEK = "energyWeek"; + public static final String DEVICE_CHANNEL_ENERGY_MONTH = "energyMonth"; + public static final String DEVICE_CHANNEL_ENERGY_YEAR = "energyYear"; + // Humidity related channels public static final String DEVICE_CHANNEL_WATER_LACKS = "waterLacking"; public static final String DEVICE_CHANNEL_HUMIDITY_HIGH = "humidityHigh"; diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncHandlerFactory.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncHandlerFactory.java index 58c30449af5..2a77f880882 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncHandlerFactory.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/VeSyncHandlerFactory.java @@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.Nullable; 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.binding.vesync.internal.handlers.VeSyncDeviceOutletHandler; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.net.http.HttpClientFactory; @@ -45,7 +46,7 @@ import org.osgi.service.component.annotations.Reference; public class VeSyncHandlerFactory extends BaseThingHandlerFactory { private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, - THING_TYPE_AIR_PURIFIER, THING_TYPE_AIR_HUMIDIFIER); + THING_TYPE_AIR_PURIFIER, THING_TYPE_AIR_HUMIDIFIER, THING_TYPE_OUTLET); private final HttpClientFactory httpClientFactory; private final TranslationProvider translationProvider; @@ -73,6 +74,8 @@ public class VeSyncHandlerFactory extends BaseThingHandlerFactory { return new VeSyncDeviceAirPurifierHandler(thing, translationProvider, localeProvider); } else if (VeSyncDeviceAirHumidifierHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { return new VeSyncDeviceAirHumidifierHandler(thing, translationProvider, localeProvider); + } else if (VeSyncDeviceOutletHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { + return new VeSyncDeviceOutletHandler(thing, translationProvider, localeProvider); } else if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { return new VeSyncBridgeHandler((Bridge) thing, httpClientFactory, translationProvider, localeProvider); } 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 a64a3c63551..e764505de59 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 @@ -12,7 +12,8 @@ */ package org.openhab.binding.vesync.internal.api; -import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*; +import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.V1_LOGIN_ENDPOINT; +import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.V1_MANAGED_DEVICES_ENDPOINT; import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; 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 0bb8e36baf9..ccc9d0cd0cd 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 @@ -23,6 +23,7 @@ import org.openhab.binding.vesync.internal.handlers.VeSyncBaseDeviceHandler; 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.binding.vesync.internal.handlers.VeSyncDeviceOutletHandler; import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryService; @@ -37,6 +38,7 @@ import org.osgi.service.component.annotations.ServiceScope; * read by the bridge, and the discovery data updated via a callback implemented by the DeviceMetaDataUpdatedHandler. * * @author David Godyear - Initial contribution + * @author Marcel Goerentz - Add support for outlets */ @NonNullByDefault @Component(scope = ServiceScope.PROTOTYPE, service = VeSyncDiscoveryService.class, configurationPid = "discovery.vesync") @@ -94,6 +96,24 @@ public class VeSyncDiscoveryService extends AbstractThingHandlerDiscoveryService @Override public void handleMetadataRetrieved(VeSyncBridgeHandler handler) { + thingHandler.getOutletMetaData().map(apMeta -> { + final Map 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_FAMILY, + VeSyncBaseDeviceHandler.getDeviceFamilyMetadata(apMeta.getDeviceType(), + VeSyncDeviceOutletHandler.DEV_TYPE_FAMILY_OUTLET, + VeSyncDeviceOutletHandler.SUPPORTED_MODEL_FAMILIES)); + 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_OUTLET, bridgeUID, deviceUUID)) + .withLabel(apMeta.getDeviceName()).withBridge(bridgeUID).withProperties(properties) + .withRepresentationProperty(DEVICE_PROP_DEVICE_MAC_ID).build(); + }).forEach(this::thingDiscovered); + thingHandler.getAirPurifiersMetadata().map(apMeta -> { final Map properties = new HashMap<>(6); final String deviceUUID = apMeta.getUuid(); 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 741ff024bf2..ceeb57ff35d 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 @@ -35,6 +35,10 @@ public interface VeSyncProtocolConstants { String DEVICE_SET_DISPLAY = "setDisplay"; String DEVICE_SET_LEVEL = "setLevel"; + // Outlet Commands + String DEVICE_GET_OUTLET_STATUS = "getOutletStatus"; + String DEVICE_GET_ENEGERGY_HISTORY = "getEnergyHistory"; + // Humidifier Commands String DEVICE_SET_AUTOMATIC_STOP = "setAutomaticStop"; String DEVICE_SET_HUMIDITY_MODE = "setHumidityMode"; diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestGetOutletStatus.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestGetOutletStatus.java new file mode 100644 index 00000000000..a30cb75b8c5 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/requests/VeSyncRequestGetOutletStatus.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.requests; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncRequestGetOutletStatus} is a Java class used as a DTO to hold the Vesync's API's common + * request data for V2 ByPass payloads. + * + * @author Marcel Goerentz - Initial contribution + */ +@NonNullByDefault +public class VeSyncRequestGetOutletStatus extends VeSyncRequest { + + @SerializedName("cid") + public String cid = ""; + + @SerializedName("configModule") + public String configModule = ""; + + @SerializedName("debugMode") + public boolean debugMode = false; + + @SerializedName("subDeviceNo") + public int subDeviceNo = 0; + + @SerializedName("token") + public String token = ""; + + @SerializedName("userCountryCode") + public String userCountryCode = ""; + + @SerializedName("deviceId") + public String deviceId = ""; + + @SerializedName("configModel") + public String configModel = ""; + + @SerializedName("payload") + public Payload payload = new Payload(); + + public class Payload { + + @SerializedName("data") + public Data data = new Data(); + + // Empty class + public class Data { + } + + @SerializedName("method") + public String method = ""; + + @SerializedName("subDeviceNo") + public int subDeviceNo = 0; + + @SerializedName("source") + public String source = "APP"; + } +} 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 7aec7ab8844..4e91456b4d5 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 @@ -53,6 +53,9 @@ public class VeSyncRequestManagedDeviceBypassV2 extends VeSyncAuthenticatedReque @SerializedName("data") public EmptyPayload data = new EmptyPayload(); + + @SerializedName("subDeviceNo") + public int subDeviceNo = 0; } public static class EmptyPayload { @@ -229,6 +232,20 @@ public class VeSyncRequestManagedDeviceBypassV2 extends VeSyncAuthenticatedReque public String mode = ""; } + public static class GetEnergyHistory extends EmptyPayload { + + public GetEnergyHistory(final long start, final long end) { + this.start = start; + this.end = end; + } + + @SerializedName("fromDay") + public long start = 0; + + @SerializedName("toDay") + public long end = 0; + } + public VeSyncRequestManagedDeviceBypassV2() { super(); method = "bypassV2"; diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassEnergyHistory.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassEnergyHistory.java new file mode 100644 index 00000000000..34f1a854d58 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassEnergyHistory.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncV2BypassEnergyHistory} is a Java class used as a DTO to hold the Vesync's API's common response + * data, in regard to an outlet device. + * + * @author Marcel Goerentz - Initial contribution + */ +public class VeSyncV2BypassEnergyHistory extends VeSyncResponse { + + @SerializedName("result") + public EnergyHistory result; + + public class EnergyHistory extends VeSyncResponse { + + @SerializedName("result") + public Result result = new Result(); + + public class Result { + + @SerializedName("energyInfos") + public List energyInfos = new ArrayList(); + + public class EnergyInfo { + + @SerializedName("timestamp") + public long timestamp = 0; + + @SerializedName("energy") + public double energy = 0.00; + } + + @SerializedName("total") + public int total = 0; + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassOutletStatus.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassOutletStatus.java new file mode 100644 index 00000000000..2e9e2c2c3a6 --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/dto/responses/VeSyncV2BypassOutletStatus.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link VeSyncV2BypassOutletStatus} is a Java class used as a DTO to hold the Vesync's API's common response + * data, in regard to an outlet device. + * + * @author Marcel Goerentz - Initial contribution + */ +public class VeSyncV2BypassOutletStatus extends VeSyncResponse { + + @SerializedName("result") + public OutletStatusResult outletResult; + + public class OutletStatusResult extends VeSyncResponse { + + @SerializedName("module") + public Object object = null; + + @SerializedName("stacktrace") + public Object object2 = null; + + @SerializedName("result") + public Result result = new Result(); + + public class Result { + + @SerializedName("enabled") + public boolean enabled = false; + + @SerializedName("voltage") + public double voltage = 0.00; + + @SerializedName("energy") + public double energy = 0.00; + + @SerializedName("power") + public double power = 0.00; + + @SerializedName("current") + public double current = 0.00; + + @SerializedName("highestVoltage") + public int highestVoltage = 0; + + @SerializedName("voltagePTStatus") + public boolean voltagePTStatus = false; + + public String getDeviceStatus() { + return enabled ? "on" : "off"; + } + } + } +} diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBridgeHandler.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBridgeHandler.java index cd3b7c3900a..4ba869b7f70 100644 --- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBridgeHandler.java +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncBridgeHandler.java @@ -12,7 +12,9 @@ */ package org.openhab.binding.vesync.internal.handlers; -import static org.openhab.binding.vesync.internal.VeSyncConstants.*; +import static org.openhab.binding.vesync.internal.VeSyncConstants.DEVICE_PROP_BRIDGE_ACCEPT_LANG; +import static org.openhab.binding.vesync.internal.VeSyncConstants.DEVICE_PROP_BRIDGE_COUNTRY_CODE; +import static org.openhab.binding.vesync.internal.VeSyncConstants.DEVICE_PROP_BRIDGE_REG_TS; import java.util.Collection; import java.util.HashMap; @@ -192,6 +194,14 @@ public class VeSyncBridgeHandler extends BaseBridgeHandler implements VeSyncClie .equals(VeSyncBaseDeviceHandler.UNKNOWN)); } + public java.util.stream.Stream<@NotNull VeSyncManagedDeviceBase> getOutletMetaData() { + return api.getMacLookupMap().values().stream() + .filter(x -> !VeSyncBaseDeviceHandler + .getDeviceFamilyMetadata(x.getDeviceType(), VeSyncDeviceOutletHandler.DEV_TYPE_FAMILY_OUTLET, + VeSyncDeviceOutletHandler.SUPPORTED_MODEL_FAMILIES) + .equals(VeSyncBaseDeviceHandler.UNKNOWN)); + } + protected void updateThings() { final VeSyncBridgeConfiguration config = getConfigAs(VeSyncBridgeConfiguration.class); getThing().getThings().forEach((th) -> updateThing(config, th.getHandler())); diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceOutletHandler.java b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceOutletHandler.java new file mode 100644 index 00000000000..f5c5b08926f --- /dev/null +++ b/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/handlers/VeSyncDeviceOutletHandler.java @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.vesync.internal.handlers; + +import static org.openhab.binding.vesync.internal.VeSyncConstants.*; +import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*; + +import java.text.ParseException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +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.VeSyncV2BypassEnergyHistory; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassEnergyHistory.EnergyHistory.Result.EnergyInfo; +import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassOutletStatus; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.MetricPrefix; +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.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link VeSyncDeviceOutletHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Marcel Goerentz - Initial contribution + */ +@NonNullByDefault +public class VeSyncDeviceOutletHandler extends VeSyncBaseDeviceHandler { + + public static final String DEV_TYPE_FAMILY_OUTLET = "OUT"; + public static final int DEFAULT_OUTLET_POLL_RATE = 60; + public static final String DEV_FAMILY_CORE_WHOG_PLUG = "WHOG"; + public static final VeSyncDeviceMetadata COREWHOPGPLUG = new VeSyncDeviceMetadata(DEV_FAMILY_CORE_WHOG_PLUG, + Arrays.asList("WHOG"), List.of("WHOGPLUG")); + public static final List SUPPORTED_MODEL_FAMILIES = Arrays.asList(COREWHOPGPLUG); + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_OUTLET); + private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceOutletHandler.class); + private final Object pollLock = new Object(); + + public VeSyncDeviceOutletHandler(Thing thing, @Reference TranslationProvider translationProvider, + @Reference LocaleProvider localeProvider) { + super(thing, translationProvider, localeProvider); + } + + @Override + public void initialize() { + super.initialize(); + customiseChannels(); + } + + @Override + public String getDeviceFamilyProtocolPrefix() { + return DEV_TYPE_FAMILY_OUTLET; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + final String deviceFamily = getThing().getProperties().get(DEVICE_PROP_DEVICE_FAMILY); + if (deviceFamily == null) { + return; + } + + scheduler.submit(() -> { + if (command instanceof OnOffType) { + if (channelUID.getId().equals(DEVICE_CHANNEL_ENABLED)) { + sendV2BypassControlCommand(DEVICE_SET_SWITCH, + new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload(command.equals(OnOffType.ON), 0)); + } + } else if (command instanceof RefreshType) { + pollForUpdate(); + } else { + logger.trace("UNKNOWN COMMAND: {} {}", command.getClass().toString(), channelUID); + } + }); + } + + @Override + public void updateBridgeBasedPolls(final VeSyncBridgeConfiguration config) { + Integer pollRate = config.outletPollInterval; + if (pollRate == null) { + pollRate = (Integer) DEFAULT_OUTLET_POLL_RATE; + } + if (ThingStatus.OFFLINE.equals(getThing().getStatus())) { + setBackgroundPollInterval(-1); + } else { + setBackgroundPollInterval(pollRate); + } + } + + @Override + protected void pollForDeviceData(ExpiringCache cachedResponse) { + processV2BypassPoll(cachedResponse); + } + + private void processV2BypassPoll(final ExpiringCache cachedResponse) { + String responseStatus = EMPTY_STRING; + String responseEnergyHistory = EMPTY_STRING; + String responses; + VeSyncV2BypassOutletStatus outletStatus; + VeSyncV2BypassEnergyHistory energyHistory; + synchronized (pollLock) { + responses = cachedResponse.getValue(); + boolean cachedDataUsed = responses != null; + if (responses == null) { + logger.trace("Requesting fresh response"); + responseStatus = sendV2BypassCommand(DEVICE_GET_OUTLET_STATUS, + new VeSyncRequestManagedDeviceBypassV2.EmptyPayload()); + + try { + long end = getTimestampForToday(); + long start = end - SECONDS_IN_MONTH; // 30 days + responseEnergyHistory = sendV2BypassCommand(DEVICE_GET_ENEGERGY_HISTORY, + new VeSyncRequestManagedDeviceBypassV2.GetEnergyHistory(start, end)); + } catch (ParseException e) { + logger.error("Could not parse timestamp: {}", e.getMessage()); + } + } else { + logger.trace("Using cached response {}", responses); + if (responses.contains("?")) { + String[] responseStrings = responses.split("/?"); + responseStatus = responseStrings[0]; + responseEnergyHistory = responseStrings[1]; + } + } + + if (responseStatus.equals(EMPTY_STRING) || responseEnergyHistory.equals(EMPTY_STRING)) { + return; + } + + outletStatus = VeSyncConstants.GSON.fromJson(responseStatus, VeSyncV2BypassOutletStatus.class); + energyHistory = VeSyncConstants.GSON.fromJson(responseEnergyHistory, VeSyncV2BypassEnergyHistory.class); + + if (outletStatus == null || energyHistory == null) { + return; + } + if (!cachedDataUsed) { + cachedResponse.putValue(responseStatus + "?" + responseEnergyHistory); + } + } + + // Bail and update the status of the thing - it will be updated to online by the next search + // that detects it is online. + if (outletStatus.isMsgDeviceOffline()) { + updateStatus(ThingStatus.OFFLINE); + return; + } else if (outletStatus.isMsgSuccess()) { + updateStatus(ThingStatus.ONLINE); + } + + if (!"0".equals(outletStatus.getCode())) { + logger.warn("{}", getLocalizedText("warning.device.unexpected-resp-for-wifi-outlet")); + return; + } + + updateState(DEVICE_CHANNEL_ENABLED, + OnOffType.from(MODE_ON.equals(outletStatus.outletResult.result.getDeviceStatus()))); + updateState(DEVICE_CHANNEL_CURRENT, new QuantityType<>(outletStatus.outletResult.result.current, Units.AMPERE)); + updateState(DEVICE_CHANNEL_VOLTAGE, new QuantityType<>(outletStatus.outletResult.result.voltage, Units.VOLT)); + updateState(DEVICE_CHANNEL_ENERGY, new QuantityType<>(outletStatus.outletResult.result.energy, Units.WATT)); + updateState(DEVICE_CHANNEL_POWER, + new QuantityType<>(outletStatus.outletResult.result.power, MetricPrefix.KILO(Units.WATT_HOUR))); + updateState(DEVICE_CHANNEL_HIGHEST_VOLTAGE, + new QuantityType<>(outletStatus.outletResult.result.highestVoltage, Units.VOLT)); + updateState(DEVICE_CHANNEL_VOLTAGE_PT_STATUS, OnOffType.from(outletStatus.outletResult.result.voltagePTStatus)); + updateState(DEVICE_CHANNEL_ENERGY_WEEK, + new QuantityType<>(getEnergy(energyHistory, 7), MetricPrefix.KILO(Units.WATT_HOUR))); + updateState(DEVICE_CHANNEL_ENERGY_MONTH, + new QuantityType<>(getEnergy(energyHistory, 30), MetricPrefix.KILO(Units.WATT_HOUR))); + } + + private static long getTimestampForToday() throws ParseException { + Instant instant = Instant.now().truncatedTo(ChronoUnit.DAYS); + return instant.toEpochMilli() / 1000; + } + + private static double getEnergy(VeSyncV2BypassEnergyHistory energyHistory, int days) { + List energyList = energyHistory.result.result.energyInfos; + double energy = 0; + for (byte i = 0; i < days; i++) { + energy += energyList.get(i).energy; + } + return energy; + } + + @Override + public List getSupportedDeviceMetadata() { + return SUPPORTED_MODEL_FAMILIES; + } +} 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 a67c8ccaa46..186c9cc5db6 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 @@ -119,5 +119,6 @@ warning.device.humidity-under = Target Humidity less than {0} - adjusting to {0} warning.device.humidity-over = Target Humidity greater than {0} - adjusting to {0} as the valid API value warning.device.humidity-mode = Humidifier mode command for {0} is not valid in the ({1}}) API possible options {2} warning.device.warm-mode-unsupported = Warm mode API is unknown in order to send the command -warning.device.unexpected-resp-for-air-purifier = Check Thing type has been set - API gave a unexpected response for an Air Purifier -warning.device.unexpected-resp-for-air-humidifier = Check Thing type has been set - API gave a unexpected response for an Air Humidifier +warning.device.unexpected-resp-for-air-purifier = Check Thing type has been set - API gave an unexpected response for an Air Purifier +warning.device.unexpected-resp-for-air-humidifier = Check Thing type has been set - API gave an unexpected response for an Air Humidifier +warning.device.unexpected-resp-for-wifi-outlet = Check Thing type has been set - API gave an unexpected response for an Outlet 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 111ca6e4f91..a7a778c3e81 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 @@ -37,9 +37,55 @@ Seconds between fetching background updates about the air purifiers / humidifiers. 60 + + + Seconds between fetching background updates about the outlets. + 60 + + + + + + + + An Outlet uplinking to VeSync + + + + + + + + + + + + + + + + + + + + + mac + + + + + The MAC of the device as reported by the API. + + + + The name allocated to the device by the app. (Must be unique if used) + + + + @@ -138,6 +184,68 @@ + + Number:ElectricCurrent + + Actual current in A + + + + + Number:Energy + + Today's energy in kWh + + + + + Number:Power + + Current power in W + + + + + Number:ElectricPotential + + Current Voltage + + + + + Number:ElectricPotential + + Highest Voltage ever measured by the outlet + + + + + Switch + + + + + + Number:Energy + + Total energy of week in kWh + + + + + Number:Energy + + Total energy of month in kWh + + + + + Number:Energy + + Total energy of year in kWh + + + Switch