[VeSync] Add support for wifi outlets (#17844)

* Add support for wifi outlets

Signed-off-by: Marcel Goerentz <m.goerentz@t-online.de>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Marcel Goerentz 2024-12-19 10:40:13 +01:00 committed by Ciprian Pascu
parent f15d09caf8
commit b005245511
15 changed files with 614 additions and 5 deletions

View File

@ -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)

View File

@ -40,4 +40,7 @@ public class VeSyncBridgeConfiguration {
*/
@Nullable
public Integer airPurifierPollInterval;
@Nullable
public Integer outletPollInterval;
}

View File

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

View File

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

View File

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

View File

@ -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<String, Object> properties = new HashMap<>(6);
final String deviceUUID = apMeta.getUuid();
properties.put(DEVICE_PROP_DEVICE_NAME, apMeta.getDeviceName());
properties.put(DEVICE_PROP_DEVICE_TYPE, apMeta.getDeviceType());
properties.put(DEVICE_PROP_DEVICE_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<String, Object> properties = new HashMap<>(6);
final String deviceUUID = apMeta.getUuid();

View File

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

View File

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

View File

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

View File

@ -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<EnergyInfo> energyInfos = new ArrayList<EnergyInfo>();
public class EnergyInfo {
@SerializedName("timestamp")
public long timestamp = 0;
@SerializedName("energy")
public double energy = 0.00;
}
@SerializedName("total")
public int total = 0;
}
}
}

View File

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

View File

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

View File

@ -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<VeSyncDeviceMetadata> SUPPORTED_MODEL_FAMILIES = Arrays.asList(COREWHOPGPLUG);
public static final Set<ThingTypeUID> 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<String> cachedResponse) {
processV2BypassPoll(cachedResponse);
}
private void processV2BypassPoll(final ExpiringCache<String> 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<EnergyInfo> 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<VeSyncDeviceMetadata> getSupportedDeviceMetadata() {
return SUPPORTED_MODEL_FAMILIES;
}
}

View File

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

View File

@ -37,9 +37,55 @@
<description>Seconds between fetching background updates about the air purifiers / humidifiers.</description>
<default>60</default>
</parameter>
<parameter name="outletPollInterval" type="integer" min="5" step="1" unit="s">
<label>Outlet Poll Rate</label>
<description>Seconds between fetching background updates about the outlets.</description>
<default>60</default>
</parameter>
</config-description>
</bridge-type>
<thing-type id="outlet">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>Outlet via VeSync</label>
<description>An Outlet uplinking to VeSync</description>
<channels>
<channel id="enabled" typeId="deviceEnabledType"/>
<channel id="current" typeId="currentType"/>
<channel id="energy" typeId="energyType"/>
<channel id="power" typeId="powerType"/>
<channel id="voltage" typeId="voltageType"/>
<channel id="highestVoltage" typeId="highestVoltageType"/>
<channel id="voltagePTStatus" typeId="voltagePTStatusType"/>
<channel id="energyWeek" typeId="energyWeekType"/>
<channel id="energyMonth" typeId="energyMonthType"/>
<channel id="energyYear" typeId="energyYearType"/>
</channels>
<properties>
<property name="deviceName"/>
<property name="deviceType"/>
<property name="mac"/>
<property name="deviceFamily"/>
</properties>
<representation-property>mac</representation-property>
<config-description>
<parameter name="mac" type="text">
<label>MAC</label>
<description>The MAC of the device as reported by the API.</description>
</parameter>
<parameter name="deviceName" type="text">
<label>Device Name</label>
<description>The name allocated to the device by the app. (Must be unique if used)</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="airPurifier">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
@ -138,6 +184,68 @@
</thing-type>
<channel-type id="currentType">
<item-type unitHint="A">Number:ElectricCurrent</item-type>
<label>Current</label>
<description>Actual current in A</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="energyType">
<item-type unitHint="kWh">Number:Energy</item-type>
<label>Energy</label>
<description>Today's energy in kWh</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="powerType">
<item-type unitHint="W">Number:Power</item-type>
<label>Power</label>
<description>Current power in W</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="voltageType">
<item-type unitHint="V">Number:ElectricPotential</item-type>
<label>Voltage</label>
<description>Current Voltage</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="highestVoltageType">
<item-type unitHint="V">Number:ElectricPotential</item-type>
<label>Highest Voltage</label>
<description>Highest Voltage ever measured by the outlet</description>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="voltagePTStatusType">
<item-type>Switch</item-type>
<label>Voltage PT Status</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="energyWeekType">
<item-type unitHint="kWh">Number:Energy</item-type>
<label>Energy Week</label>
<description>Total energy of week in kWh</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="energyMonthType">
<item-type unitHint="kWh">Number:Energy</item-type>
<label>Energy Month</label>
<description>Total energy of month in kWh</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="energyYearType">
<item-type unitHint="kWh">Number:Energy</item-type>
<label>Energy Year</label>
<description>Total energy of year in kWh</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="deviceEnabledType">
<item-type>Switch</item-type>
<label>Switched On</label>