mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[tuya] Add support for quantities (#18710)
* Add support for quantities Signed-off-by: Mike Jagdis <mjagdis@eris-associates.co.uk>
This commit is contained in:
@@ -108,9 +108,9 @@ The `dp2` parameter identifies the ON/OFF switch that is usually available on di
|
||||
The `reversed` parameter changes the direction of the scale (e.g. 0 becomes 100, 100 becomes 0).
|
||||
It defaults to `false`.
|
||||
|
||||
### Type `number`
|
||||
### Type `number/quantity`
|
||||
|
||||
The `number` channel has two additional mandatory parameters `min` and `max`.
|
||||
The `number` and `quantity` channels have two additional mandatory parameters `min` and `max`.
|
||||
The `min` and `max` parameters define the range allowed (e.g. 0-86400 for turn-off "countdown").
|
||||
|
||||
### Type `string`
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ public class TuyaBindingConstants {
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_UID_COLOR = new ChannelTypeUID(BINDING_ID, "color");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_UID_DIMMER = new ChannelTypeUID(BINDING_ID, "dimmer");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_UID_NUMBER = new ChannelTypeUID(BINDING_ID, "number");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_UID_QUANTITY = new ChannelTypeUID(BINDING_ID, "quantity");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_UID_STRING = new ChannelTypeUID(BINDING_ID, "string");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_UID_SWITCH = new ChannelTypeUID(BINDING_ID, "switch");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_UID_IR_CODE = new ChannelTypeUID(BINDING_ID, "ir-code");
|
||||
|
||||
+17
-2
@@ -12,12 +12,17 @@
|
||||
*/
|
||||
package org.openhab.binding.tuya.internal;
|
||||
|
||||
import static org.openhab.binding.tuya.internal.TuyaBindingConstants.SCHEMAS;
|
||||
import static org.openhab.binding.tuya.internal.TuyaBindingConstants.THING_TYPE_PROJECT;
|
||||
import static org.openhab.binding.tuya.internal.TuyaBindingConstants.THING_TYPE_TUYA_DEVICE;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@@ -96,8 +101,18 @@ public class TuyaHandlerFactory extends BaseThingHandlerFactory {
|
||||
if (THING_TYPE_PROJECT.equals(thingTypeUID)) {
|
||||
return new ProjectHandler(thing, httpClient, storage, gson);
|
||||
} else if (THING_TYPE_TUYA_DEVICE.equals(thingTypeUID)) {
|
||||
return new TuyaDeviceHandler(thing, gson.fromJson(storage.get(thing.getUID().getId()), STORAGE_TYPE), gson,
|
||||
dynamicCommandDescriptionProvider, eventLoopGroup, udpDiscoveryListener);
|
||||
// stored schemas are usually more complete
|
||||
Map<String, SchemaDp> schemaDps = SCHEMAS.get(thing.getConfiguration().get("productId"));
|
||||
if (schemaDps == null) {
|
||||
// fallback to retrieved schema
|
||||
List<SchemaDp> listDps = Objects.requireNonNullElse(
|
||||
gson.fromJson(storage.get(thing.getUID().getId()), STORAGE_TYPE), //
|
||||
List.of());
|
||||
schemaDps = listDps.stream()
|
||||
.collect(Collectors.toMap(s -> s.code, s -> s, (e1, e2) -> e1, LinkedHashMap::new));
|
||||
}
|
||||
return new TuyaDeviceHandler(thing, schemaDps, gson, dynamicCommandDescriptionProvider, eventLoopGroup,
|
||||
udpDiscoveryListener);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
+3
@@ -54,6 +54,9 @@ public class DeviceSchema {
|
||||
public static class NumericRange {
|
||||
public double min = Double.MIN_VALUE;
|
||||
public double max = Double.MAX_VALUE;
|
||||
public int scale = 0;
|
||||
public int step = 1;
|
||||
public String unit = "";
|
||||
}
|
||||
|
||||
public boolean hasFunction(String fcn) {
|
||||
|
||||
+88
-25
@@ -16,10 +16,12 @@ import static org.openhab.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYP
|
||||
import static org.openhab.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_DIMMER;
|
||||
import static org.openhab.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_IR_CODE;
|
||||
import static org.openhab.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_NUMBER;
|
||||
import static org.openhab.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_QUANTITY;
|
||||
import static org.openhab.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_STRING;
|
||||
import static org.openhab.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_SWITCH;
|
||||
import static org.openhab.binding.tuya.internal.TuyaBindingConstants.SCHEMAS;
|
||||
import static org.openhab.core.library.CoreItemFactory.NUMBER;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
@@ -34,6 +36,8 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.tuya.internal.config.ChannelConfiguration;
|
||||
@@ -54,6 +58,7 @@ import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
@@ -72,6 +77,7 @@ import org.openhab.core.types.CommandOption;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.openhab.core.types.util.UnitUtils;
|
||||
import org.openhab.core.util.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -100,7 +106,7 @@ public class TuyaDeviceHandler extends BaseThingHandler implements DeviceInfoSub
|
||||
private final EventLoopGroup eventLoopGroup;
|
||||
private DeviceConfiguration configuration = new DeviceConfiguration();
|
||||
private @Nullable TuyaDevice tuyaDevice;
|
||||
private final List<SchemaDp> schemaDps;
|
||||
private final Map<String, SchemaDp> schemaDps;
|
||||
private boolean oldColorMode = false;
|
||||
|
||||
private @Nullable ScheduledFuture<?> reconnectFuture;
|
||||
@@ -117,15 +123,15 @@ public class TuyaDeviceHandler extends BaseThingHandler implements DeviceInfoSub
|
||||
Duration.ofSeconds(10));
|
||||
private final Map<String, State> channelStateCache = new HashMap<>();
|
||||
|
||||
public TuyaDeviceHandler(Thing thing, @Nullable List<SchemaDp> schemaDps, Gson gson,
|
||||
public TuyaDeviceHandler(Thing thing, Map<String, SchemaDp> schemaDps, Gson gson,
|
||||
BaseDynamicCommandDescriptionProvider dynamicCommandDescriptionProvider, EventLoopGroup eventLoopGroup,
|
||||
UdpDiscoveryListener udpDiscoveryListener) {
|
||||
super(thing);
|
||||
this.schemaDps = schemaDps;
|
||||
this.gson = gson;
|
||||
this.udpDiscoveryListener = udpDiscoveryListener;
|
||||
this.eventLoopGroup = eventLoopGroup;
|
||||
this.dynamicCommandDescriptionProvider = dynamicCommandDescriptionProvider;
|
||||
this.schemaDps = Objects.requireNonNullElse(schemaDps, List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -185,6 +191,30 @@ public class TuyaDeviceHandler extends BaseThingHandler implements DeviceInfoSub
|
||||
return;
|
||||
} else if (value instanceof String string && CHANNEL_TYPE_UID_NUMBER.equals(channelTypeUID)) {
|
||||
updateState(channelId, new DecimalType(string));
|
||||
return;
|
||||
} else if ((Double.class.isAssignableFrom(value.getClass()) || value instanceof String)
|
||||
&& CHANNEL_TYPE_UID_QUANTITY.equals(channelTypeUID)) {
|
||||
BigDecimal d;
|
||||
if (value instanceof String stringValue) {
|
||||
d = new BigDecimal(stringValue);
|
||||
} else {
|
||||
d = new BigDecimal((double) value);
|
||||
}
|
||||
|
||||
SchemaDp schemaDp = schemaDps.get(channelId);
|
||||
|
||||
if (schemaDp != null) {
|
||||
d = d.movePointLeft(schemaDp.scale);
|
||||
|
||||
Unit<?> unit = schemaDp.parsedUnit;
|
||||
if (unit != null) {
|
||||
updateState(channelId, new QuantityType<>(d, unit));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
updateState(channelId, new DecimalType(d));
|
||||
return;
|
||||
} else if (Boolean.class.isAssignableFrom(value.getClass())
|
||||
&& CHANNEL_TYPE_UID_SWITCH.equals(channelTypeUID)) {
|
||||
updateState(channelId, OnOffType.from((boolean) value));
|
||||
@@ -337,7 +367,28 @@ public class TuyaDeviceHandler extends BaseThingHandler implements DeviceInfoSub
|
||||
}
|
||||
} else if (CHANNEL_TYPE_UID_STRING.equals(channelTypeUID)) {
|
||||
commandRequest.put(configuration.dp, command.toString());
|
||||
} else if (CHANNEL_TYPE_UID_NUMBER.equals(channelTypeUID)) {
|
||||
} else if (CHANNEL_TYPE_UID_QUANTITY.equals(channelTypeUID) || CHANNEL_TYPE_UID_NUMBER.equals(channelTypeUID)) {
|
||||
if (command instanceof QuantityType quantityType) {
|
||||
SchemaDp schemaDp = schemaDps.get(channelUID.getId());
|
||||
|
||||
if (schemaDp != null && !schemaDp.unit.isEmpty()) {
|
||||
// If the item type for the channel is not dimensioned the unit is not usable and we
|
||||
// assume whoever sent a quantity instead of a bare number knows what they are doing.
|
||||
Channel channel = thing.getChannel(channelUID.getId());
|
||||
if (channel != null && !NUMBER.equals(channel.getAcceptedItemType())) {
|
||||
quantityType = quantityType.toUnit(schemaDp.unit);
|
||||
}
|
||||
}
|
||||
|
||||
if (quantityType != null) {
|
||||
BigDecimal d = quantityType.toBigDecimal();
|
||||
if (schemaDp != null) {
|
||||
d = d.movePointRight(schemaDp.scale);
|
||||
}
|
||||
command = new DecimalType(d);
|
||||
}
|
||||
}
|
||||
|
||||
if (command instanceof DecimalType decimalType) {
|
||||
commandRequest.put(configuration.dp,
|
||||
configuration.sendAsString ? String.format("%d", decimalType.intValue())
|
||||
@@ -428,21 +479,7 @@ public class TuyaDeviceHandler extends BaseThingHandler implements DeviceInfoSub
|
||||
|
||||
// check if we have channels and add them if available
|
||||
if (thing.getChannels().isEmpty()) {
|
||||
// stored schemas are usually more complete
|
||||
Map<String, SchemaDp> schema = SCHEMAS.get(configuration.productId);
|
||||
if (schema == null) {
|
||||
if (!schemaDps.isEmpty()) {
|
||||
// fallback to retrieved schema
|
||||
schema = schemaDps.stream()
|
||||
.collect(Collectors.toMap(s -> s.code, s -> s, (e1, e2) -> e1, LinkedHashMap::new));
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"No channels added and schema not found.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
addChannels(schema);
|
||||
addChannels();
|
||||
}
|
||||
|
||||
thing.getChannels().forEach(this::configureChannel);
|
||||
@@ -471,7 +508,7 @@ public class TuyaDeviceHandler extends BaseThingHandler implements DeviceInfoSub
|
||||
configuration.localKey.getBytes(StandardCharsets.UTF_8), deviceInfo.ip, deviceInfo.protocolVersion);
|
||||
}
|
||||
|
||||
private void addChannels(Map<String, SchemaDp> schema) {
|
||||
private void addChannels() {
|
||||
ThingBuilder thingBuilder = editThing();
|
||||
ThingUID thingUID = thing.getUID();
|
||||
ThingHandlerCallback callback = getCallback();
|
||||
@@ -481,12 +518,13 @@ public class TuyaDeviceHandler extends BaseThingHandler implements DeviceInfoSub
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Channel> channels = new LinkedHashMap<>(schema.entrySet().stream().map(e -> {
|
||||
Map<String, Channel> channels = new LinkedHashMap<>(schemaDps.entrySet().stream().map(e -> {
|
||||
String channelId = e.getKey();
|
||||
SchemaDp schemaDp = e.getValue();
|
||||
|
||||
ChannelUID channelUID = new ChannelUID(thingUID, channelId);
|
||||
Map<String, @Nullable Object> configuration = new HashMap<>();
|
||||
String acceptedItemType = null;
|
||||
Map<@Nullable String, @Nullable Object> configuration = new HashMap<>();
|
||||
configuration.put("dp", schemaDp.id);
|
||||
|
||||
ChannelTypeUID channeltypeUID;
|
||||
@@ -508,6 +546,28 @@ public class TuyaDeviceHandler extends BaseThingHandler implements DeviceInfoSub
|
||||
channeltypeUID = CHANNEL_TYPE_UID_NUMBER;
|
||||
configuration.put("min", schemaDp.min);
|
||||
configuration.put("max", schemaDp.max);
|
||||
|
||||
if (schemaDp.scale > 0 || !schemaDp.unit.isEmpty()) {
|
||||
channeltypeUID = CHANNEL_TYPE_UID_QUANTITY;
|
||||
|
||||
if (!schemaDp.unit.isEmpty()) {
|
||||
Unit<?> unit = schemaDp.parsedUnit;
|
||||
if (unit == null) {
|
||||
unit = UnitUtils.parseUnit(schemaDp.unit);
|
||||
schemaDp.parsedUnit = unit;
|
||||
}
|
||||
|
||||
if (unit != null) {
|
||||
String dimension = UnitUtils.getDimensionName(unit);
|
||||
if (dimension != null) {
|
||||
acceptedItemType = "Number:" + dimension;
|
||||
} else {
|
||||
logger.warn("{} has unit \"{}\" but openHAB doesn't know the dimension", channelId,
|
||||
schemaDp.unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// e.g. type "raw", add empty channel
|
||||
return Map.entry("", ChannelBuilder.create(channelUID).build());
|
||||
@@ -525,8 +585,11 @@ public class TuyaDeviceHandler extends BaseThingHandler implements DeviceInfoSub
|
||||
}
|
||||
}
|
||||
|
||||
return Map.entry(channelId, callback.createChannelBuilder(channelUID, channeltypeUID)
|
||||
.withLabel(schemaDp.label).withConfiguration(new Configuration(configuration)).build());
|
||||
return Map.entry(channelId, callback.createChannelBuilder(channelUID, channeltypeUID) //
|
||||
.withAcceptedItemType(acceptedItemType) //
|
||||
.withLabel(schemaDp.label) //
|
||||
.withConfiguration(new Configuration(configuration)) //
|
||||
.build());
|
||||
}).filter(c -> !c.getKey().isEmpty()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
|
||||
|
||||
List<String> channelSuffixes = List.of("", "_1", "_2");
|
||||
|
||||
+359
@@ -16,9 +16,12 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.tuya.internal.cloud.dto.DeviceSchema;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
@@ -40,9 +43,12 @@ public class SchemaDp {
|
||||
public String code = "";
|
||||
public String type = "";
|
||||
public String label = "";
|
||||
public String unit = "";
|
||||
public @Nullable Double min;
|
||||
public @Nullable Double max;
|
||||
public Integer scale = 0;
|
||||
public @Nullable List<String> range;
|
||||
public @Nullable Unit<?> parsedUnit;
|
||||
|
||||
public static SchemaDp fromRemoteSchema(Gson gson, DeviceSchema.Description function) {
|
||||
SchemaDp schemaDp = new SchemaDp();
|
||||
@@ -59,8 +65,361 @@ public class SchemaDp {
|
||||
gson.fromJson(function.values.replaceAll("\\\\", ""), DeviceSchema.NumericRange.class));
|
||||
schemaDp.min = numericRange.min;
|
||||
schemaDp.max = numericRange.max;
|
||||
|
||||
if (numericRange.scale == 1 && numericRange.min == 0 && numericRange.max == 1 && numericRange.step == 1) {
|
||||
LoggerFactory.getLogger(SchemaDp.class).warn("Ignoring scale=1 when min=0, max=1, step=1 for {}",
|
||||
schemaDp.code);
|
||||
} else {
|
||||
schemaDp.scale = numericRange.scale;
|
||||
}
|
||||
|
||||
schemaDp.unit = normalizeUnit(schemaDp.code, numericRange);
|
||||
}
|
||||
|
||||
return schemaDp;
|
||||
}
|
||||
|
||||
// This is very like the normalizeUnit in convert.js. We really only need to deal with _likely_ garbage
|
||||
// in _remote_ schemas here because the local schemas are handled by convert.js however assuming the
|
||||
// same mistakes aren't going to be repeated ad infinitum is probably unwise.
|
||||
private static String normalizeUnit(String code, DeviceSchema.NumericRange properties) {
|
||||
// Identifiers may be suffixed with an instance number if it's repeated.
|
||||
code = code.replaceFirst("_?\\d+$", "");
|
||||
|
||||
String unit = properties.unit //
|
||||
.replaceAll("[ .·]+", "") // Remove all forms of space and dots.
|
||||
.replaceAll("μ", "µ") // Unicode has both the Greek letter and a micro sign - we prefer the latter.
|
||||
.replaceAll("[˚º]", "°") // Degree sign rather than ring above or masculine ordinal indicator.
|
||||
.replaceAll("㎡", "m²") // Avoid the single character m squared.
|
||||
.replaceAll("(?<=[^-\\d])2((?!\\d)|$)", "²") // Superscript 2s.
|
||||
.replaceAll("(?<=[^-\\d])3((?!\\d)|$)", "³") // Superscript 3s.
|
||||
.replaceAll("℃", "°C") // Prefer two characters for degree Celsius.
|
||||
.replaceAll("°c", "°C") // ... and capitalized.
|
||||
.replaceAll("℉", "°F") // Prefer two characters for degree Fahrenheit.
|
||||
.replaceAll("°f", "°F") // ... and capitalized.
|
||||
;
|
||||
|
||||
// If we are given a unit try and translate it into something comprehensible.
|
||||
switch (unit) {
|
||||
case "0":
|
||||
case "1":
|
||||
case "Number":
|
||||
case "step":
|
||||
case "times":
|
||||
case "x":
|
||||
case "X":
|
||||
case "PF": // Used as an abbreviation for power factor.
|
||||
case "pf":
|
||||
case "ppt": // Probably Parts Per Thousand but not available as a unit in openHAB.
|
||||
case "SG": // Specific Gravity but not available as a unit in openHAB.
|
||||
case "sg":
|
||||
case "份": // share/part/copy/portion
|
||||
case "次": // number
|
||||
case "段": // part/segment
|
||||
case "无": // none
|
||||
case "步": // step/pace/footsteps
|
||||
case "元": // Yuan
|
||||
return "";
|
||||
|
||||
case "%RH":
|
||||
case "RH%":
|
||||
case "%":
|
||||
case "%LEL":
|
||||
case "LEL":
|
||||
case "百分比(%)":
|
||||
return "%";
|
||||
|
||||
case "°C/°F":
|
||||
case "°C°F":
|
||||
case "°C/F":
|
||||
case "摄氏度或华氏度":
|
||||
// FIXME: some devices seem to be switchable between °C and °F. We may
|
||||
// need code in the handler to deal with these.
|
||||
// return "°C°F"
|
||||
return "";
|
||||
|
||||
case "01s":
|
||||
switch (properties.scale) {
|
||||
case 0:
|
||||
properties.scale = 1;
|
||||
case 1:
|
||||
return "s";
|
||||
}
|
||||
break;
|
||||
|
||||
case "Amp":
|
||||
return "A";
|
||||
|
||||
case "mAh":
|
||||
switch (code) {
|
||||
case "cur_voltage": // At least one product has a cur_voltage with a unit of "mAh"!
|
||||
return "V";
|
||||
}
|
||||
return unit;
|
||||
|
||||
case "C": // FIXME: could be Coulombs...?
|
||||
case "c":
|
||||
case "摄氏度":
|
||||
return "°C";
|
||||
|
||||
case "CM":
|
||||
case "厘米":
|
||||
return "cm";
|
||||
|
||||
case "day":
|
||||
case "天":
|
||||
return "d";
|
||||
|
||||
case "F": // FIXME: could be Farads...?
|
||||
case "华氏度":
|
||||
return "°F";
|
||||
|
||||
case "mg/L":
|
||||
return "mg/l";
|
||||
|
||||
case "µg/m³":
|
||||
case "ug/m³":
|
||||
case "um/m³": // Seen on some particulate matter standard functions.
|
||||
return "µg/m³";
|
||||
|
||||
case "HZ":
|
||||
return "Hz";
|
||||
|
||||
case "hour":
|
||||
case "H":
|
||||
case "Hour":
|
||||
case "小时":
|
||||
case "小时(暂定)":
|
||||
return "h";
|
||||
|
||||
case "hPa/mb":
|
||||
return "hPa";
|
||||
|
||||
case "inch":
|
||||
return "in";
|
||||
|
||||
case "Kcal":
|
||||
return "kcal";
|
||||
|
||||
case "KG":
|
||||
case "Kg":
|
||||
return "kg";
|
||||
|
||||
case "Klux":
|
||||
return "klx";
|
||||
|
||||
case "KM":
|
||||
return "km";
|
||||
|
||||
case "KM/h":
|
||||
return "km/h";
|
||||
|
||||
case "Kpa":
|
||||
return "kPa";
|
||||
|
||||
case "kVar":
|
||||
case "Kvar":
|
||||
case "KVar":
|
||||
return "kvar";
|
||||
|
||||
case "kVarh":
|
||||
case "Kvarh":
|
||||
case "KVarh":
|
||||
return "kvarh";
|
||||
|
||||
case "kw":
|
||||
case "Kw":
|
||||
case "KW":
|
||||
return "kW";
|
||||
|
||||
case "kwh":
|
||||
case "Kwh":
|
||||
case "KwH":
|
||||
case "KWh":
|
||||
case "KWH":
|
||||
case "kW*H":
|
||||
case "kW*h":
|
||||
case "KW*H":
|
||||
case "k-Wh":
|
||||
return "kWh";
|
||||
|
||||
case "L":
|
||||
case "升":
|
||||
case "升(L)":
|
||||
return "l";
|
||||
|
||||
case "L/min":
|
||||
case "升/分钟":
|
||||
return "l/min";
|
||||
|
||||
case "lux":
|
||||
case "Lux":
|
||||
case "LUX":
|
||||
return "lx";
|
||||
|
||||
case "M":
|
||||
case "米":
|
||||
return "m";
|
||||
|
||||
case "mL":
|
||||
return "ml";
|
||||
|
||||
case "M²":
|
||||
return "m²";
|
||||
|
||||
case "立方米":
|
||||
return "m³";
|
||||
|
||||
case "Mhz":
|
||||
return "MHz";
|
||||
|
||||
case "Min":
|
||||
case "MIN":
|
||||
case "mins":
|
||||
case "minute":
|
||||
case "Minute":
|
||||
case "minutes":
|
||||
case "mintues":
|
||||
case "分钟":
|
||||
case "分钟m":
|
||||
return "min";
|
||||
|
||||
case "Mpa":
|
||||
return "MPa";
|
||||
|
||||
case "MPH":
|
||||
return "mph";
|
||||
|
||||
case "ph":
|
||||
return "pH";
|
||||
|
||||
case "PPM":
|
||||
return "ppm";
|
||||
|
||||
case "PSI":
|
||||
return "psi";
|
||||
|
||||
case "RPM":
|
||||
case "r/s":
|
||||
return "rpm";
|
||||
|
||||
case "s":
|
||||
case "S": // Siemens or seconds?
|
||||
if (code.startsWith("ec")) { // ec = Electrical Conductance
|
||||
return "S";
|
||||
}
|
||||
return "s";
|
||||
|
||||
case "sec":
|
||||
case "Sec":
|
||||
case "second":
|
||||
case "Seconds":
|
||||
case "seconds":
|
||||
case "SECOND":
|
||||
case "秒":
|
||||
return "s";
|
||||
|
||||
case "µs":
|
||||
case "µS":
|
||||
case "uS":
|
||||
case "us": // Siemens or seconds?
|
||||
if (code.startsWith("ec")) { // ec = Electrical Conductance
|
||||
return "µS";
|
||||
}
|
||||
return "µs";
|
||||
|
||||
case "mS/cm":
|
||||
case "ms/cm": // Siemens or seconds? ms/cm seems unlikely...
|
||||
return "mS/cm";
|
||||
|
||||
case "uS/cm":
|
||||
case "us/cm": // Siemens or seconds? µs/cm seems unlikely...
|
||||
return "µS/cm";
|
||||
|
||||
case "v":
|
||||
case "VAC":
|
||||
case "VDC":
|
||||
return "V";
|
||||
|
||||
case "w":
|
||||
case "Watt":
|
||||
return "W";
|
||||
|
||||
case "wh":
|
||||
return "Wh";
|
||||
|
||||
case "欧姆":
|
||||
return "Ω";
|
||||
|
||||
case "度": // degree/time/counter for ...
|
||||
switch (code) {
|
||||
case "KCWD":
|
||||
case "JLWD":
|
||||
case "SSWD": // window open/comfort/econ temperatures with min=5.0 max=30.0
|
||||
return "°C";
|
||||
}
|
||||
break;
|
||||
|
||||
case "分": // point/minute/fraction/part/tenth
|
||||
switch (code) {
|
||||
case "alarm_sound_duration":
|
||||
case "Clean_time":
|
||||
case "clean_time":
|
||||
return "min";
|
||||
}
|
||||
break;
|
||||
|
||||
case "档": // files/pigeonhole
|
||||
switch (code) {
|
||||
case "fan_speed":
|
||||
return "rpm";
|
||||
|
||||
case "sensitivity":
|
||||
case "move_sensitivity":
|
||||
case "presence_sensitivity":
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Not all standard functions have a default unit set and not all developers set one.
|
||||
// Not all custom functions are... well... sane.
|
||||
switch (code) {
|
||||
case "add_ele":
|
||||
if (properties.scale == 3) {
|
||||
return "kWh";
|
||||
} else if (properties.scale == 0) {
|
||||
return "Wh";
|
||||
}
|
||||
break;
|
||||
|
||||
case "countdown":
|
||||
if (properties.min == 0 && properties.max == 86400) {
|
||||
return "s";
|
||||
}
|
||||
break;
|
||||
|
||||
case "time_zone":
|
||||
// A couple of products say unit="z" but have a range of 0-23. Mind you, no negative
|
||||
// offset and scale=0, step=1 so it has restricted utility anyway.
|
||||
if (properties.min == 0 && properties.max == 23) {
|
||||
return "h";
|
||||
} else {
|
||||
switch (unit) {
|
||||
case "z":
|
||||
return "";
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "wind_dir360":
|
||||
if (properties.min == 0 && (properties.max == 360 || properties.max == 361)) {
|
||||
return "°";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Use what it said. The unit will get checked for usability when the Channels are added to Things.
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +197,33 @@
|
||||
</config-description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="quantity">
|
||||
<item-type>Number</item-type>
|
||||
<label>Number</label>
|
||||
|
||||
<category>Number</category>
|
||||
|
||||
<state pattern="%.3f %unit%"></state>
|
||||
|
||||
<config-description>
|
||||
<parameter name="dp" type="integer" required="true">
|
||||
<label>DP</label>
|
||||
</parameter>
|
||||
<parameter name="min" type="integer">
|
||||
<label>Minimum</label>
|
||||
</parameter>
|
||||
<parameter name="max" type="integer">
|
||||
<label>Maximum</label>
|
||||
</parameter>
|
||||
<parameter name="sendAsString" type="boolean">
|
||||
<label>Send As String</label>
|
||||
<description>Send the value as string instead of number.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="string">
|
||||
<item-type>String</item-type>
|
||||
<label>String</label>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,401 @@
|
||||
const http = require('https');
|
||||
const fs = require('fs');
|
||||
|
||||
function normalizeUnit(productKey, orig_code, properties, orig_unit) {
|
||||
// Identifiers may be suffixed with an instance number if it's repeated.
|
||||
let code = orig_code.replaceAll(/_?\d+$/g, "");
|
||||
|
||||
let unit = orig_unit
|
||||
.replaceAll(/[ .·]+/g, "") // Remove all forms of space and dots.
|
||||
.replaceAll(/μ/g, "µ") // Unicode has both the Greek letter and a micro sign - we prefer the latter.
|
||||
.replaceAll(/[˚º]/g, "°") // Degree sign rather than ring above or masculine ordinal indicator.
|
||||
.replaceAll(/㎡/g, "m²") // Avoid the single character m squared.
|
||||
.replaceAll(/(?<=[^-\d])2((?!\d)|$)/g, "²") // Superscript 2s
|
||||
.replaceAll(/(?<=[^-\d])3((?!\d)|$)/g, "³") // Superscript 3s
|
||||
.replaceAll(/℃/g, "°C") // Prefer two characters for degree Celsius.
|
||||
.replaceAll(/°c/g, "°C") // ... and capitalized.
|
||||
.replaceAll(/℉/g, "°F") // Prefer two characters for degree Fahrenheit.
|
||||
.replaceAll(/°f/g, "°F") // ... and capitalized.
|
||||
;
|
||||
|
||||
// If we are given a unit try and translate it into something comprehensible.
|
||||
switch (unit) {
|
||||
// Standard units accepted as-is. Anything not explicitly matched here, or fixed
|
||||
// up elsewhere, will cause an "Unsure ..." message to be logged and the unit will
|
||||
// be omitted from the curated schema.
|
||||
// Note that some standard units used are not listed here because they have special
|
||||
// handling further down.
|
||||
case "°": case "%": case "°C": case "°F": case "Ω":
|
||||
case "A": case "mA":
|
||||
case "Ah":
|
||||
case "bar": case "mbar":
|
||||
case "cal": case "kcal":
|
||||
case "d": case "h": case "min": case "ms": // "s" has special handling below
|
||||
case "dBm":
|
||||
case "g": case "kg": case "mg": case "mg/m³":
|
||||
case "gal": case "gal/h": case "gal/min":
|
||||
case "Hz": case "kHz": case "MHz":
|
||||
case "in":
|
||||
case "l": case "ml": case "l/min": case "ml/day":
|
||||
case "lx":
|
||||
case "m": case "m²": case "m³": case "m/s": case "cm": case "km": case "km/h": case "mm":
|
||||
case "mph":
|
||||
case "Nm":
|
||||
case "Pa": case "hPa": case "kPa":
|
||||
case "pH":
|
||||
case "ppm":
|
||||
case "psi":
|
||||
case "rpm":
|
||||
case "µS/cm":
|
||||
case "V": case "mV":
|
||||
case "var": case "kvar": case "kvarh":
|
||||
case "W": case "Wh": case "kW": case "kWh":
|
||||
return unit;
|
||||
|
||||
case "0":
|
||||
case "1":
|
||||
case "Number":
|
||||
case "step":
|
||||
case "times":
|
||||
case "x":
|
||||
case "X":
|
||||
case "PF": // Used as an abbreviation for power factor.
|
||||
case "pf":
|
||||
case "ppt": // Probably Parts Per Thousand but not available as a unit in openHAB.
|
||||
case "SG": // Specific Gravity but not available as a unit in openHAB.
|
||||
case "sg":
|
||||
case "份": // share/part/copy/portion
|
||||
case "次": // number
|
||||
case "段": // part/segment
|
||||
case "无": // none
|
||||
case "步": // step/pace/footsteps
|
||||
case "元": // Yuan
|
||||
return "";
|
||||
|
||||
case "%RH":
|
||||
case "RH%":
|
||||
case "%":
|
||||
case "%LEL":
|
||||
case "LEL":
|
||||
case "百分比(%)":
|
||||
return "%";
|
||||
|
||||
case "°C/°F":
|
||||
case "°C°F":
|
||||
case "°C/F":
|
||||
case "摄氏度或华氏度":
|
||||
// FIXME: some devices seem to be switchable between °C and °F. We may
|
||||
// need code in the handler to deal with these.
|
||||
// return "°C°F"
|
||||
return "";
|
||||
|
||||
case "01s":
|
||||
switch (properties.scale) {
|
||||
case 0:
|
||||
properties.scale = 1;
|
||||
case 1:
|
||||
return "s";
|
||||
}
|
||||
break;
|
||||
|
||||
case "Amp":
|
||||
return "A";
|
||||
|
||||
case "mAh":
|
||||
switch (code) {
|
||||
case "cur_voltage": // At least one product has a cur_voltage with a unit of "mAh"!
|
||||
return "V";
|
||||
}
|
||||
return unit;
|
||||
|
||||
case "C": // FIXME: could be Coulombs...?
|
||||
case "c":
|
||||
case "摄氏度":
|
||||
return "°C";
|
||||
|
||||
case "CM":
|
||||
case "厘米":
|
||||
return "cm";
|
||||
|
||||
case "day":
|
||||
case "天":
|
||||
return "d";
|
||||
|
||||
case "F": // FIXME: could be Farads...?
|
||||
case "华氏度":
|
||||
return "°F";
|
||||
|
||||
case "mg/L":
|
||||
return "mg/l";
|
||||
|
||||
case "µg/m³":
|
||||
case "ug/m³":
|
||||
case "um/m³": // Seen on some particulate matter standard functions.
|
||||
return "µg/m³";
|
||||
|
||||
case "HZ":
|
||||
return "Hz";
|
||||
|
||||
case "hour":
|
||||
case "H":
|
||||
case "Hour":
|
||||
case "小时":
|
||||
case "小时(暂定)":
|
||||
return "h";
|
||||
|
||||
case "hPa/mb":
|
||||
return "hPa";
|
||||
|
||||
case "inch":
|
||||
return "in";
|
||||
|
||||
case "Kcal":
|
||||
return "kcal";
|
||||
|
||||
case "KG":
|
||||
case "Kg":
|
||||
return "kg";
|
||||
|
||||
case "Klux":
|
||||
return "klx";
|
||||
|
||||
case "KM":
|
||||
return "km";
|
||||
|
||||
case "KM/h":
|
||||
return "km/h";
|
||||
|
||||
case "Kpa":
|
||||
return "kPa";
|
||||
|
||||
case "kVar":
|
||||
case "Kvar":
|
||||
case "KVar":
|
||||
return "kvar";
|
||||
|
||||
case "kVarh":
|
||||
case "Kvarh":
|
||||
case "KVarh":
|
||||
return "kvarh";
|
||||
|
||||
case "kw":
|
||||
case "Kw":
|
||||
case "KW":
|
||||
return "kW";
|
||||
|
||||
case "kwh":
|
||||
case "Kwh":
|
||||
case "KwH":
|
||||
case "KWh":
|
||||
case "KWH":
|
||||
case "kW*H":
|
||||
case "kW*h":
|
||||
case "KW*H":
|
||||
case "k-Wh":
|
||||
return "kWh";
|
||||
|
||||
case "L":
|
||||
case "升":
|
||||
case "升(L)":
|
||||
return "l";
|
||||
|
||||
case "L/min":
|
||||
case "升/分钟":
|
||||
return "l/min";
|
||||
|
||||
case "lux":
|
||||
case "Lux":
|
||||
case "LUX":
|
||||
return "lx";
|
||||
|
||||
case "M":
|
||||
case "米":
|
||||
return "m";
|
||||
|
||||
case "mL":
|
||||
return "ml";
|
||||
|
||||
case "M²":
|
||||
return "m²";
|
||||
|
||||
case "立方米":
|
||||
return "m³";
|
||||
|
||||
case "Mhz":
|
||||
return "MHz";
|
||||
|
||||
case "Min":
|
||||
case "MIN":
|
||||
case "mins":
|
||||
case "minute":
|
||||
case "Minute":
|
||||
case "minutes":
|
||||
case "mintues":
|
||||
case "分钟":
|
||||
case "分钟m":
|
||||
return "min";
|
||||
|
||||
case "Mpa":
|
||||
return "MPa";
|
||||
|
||||
case "MPH":
|
||||
return "mph";
|
||||
|
||||
case "ph":
|
||||
return "pH";
|
||||
|
||||
case "PPM":
|
||||
return "ppm";
|
||||
|
||||
case "PSI":
|
||||
return "psi";
|
||||
|
||||
case "RPM":
|
||||
case "r/s":
|
||||
return "rpm";
|
||||
|
||||
case "s":
|
||||
case "S": // Siemens or seconds?
|
||||
if (code.startsWith("ec")) { // ec = Electrical Conductance
|
||||
return "S";
|
||||
}
|
||||
return "s";
|
||||
|
||||
case "sec":
|
||||
case "Sec":
|
||||
case "second":
|
||||
case "Seconds":
|
||||
case "seconds":
|
||||
case "SECOND":
|
||||
case "秒":
|
||||
return "s";
|
||||
|
||||
case "µs":
|
||||
case "µS":
|
||||
case "uS":
|
||||
case "us": // Siemens or seconds?
|
||||
if (code.startsWith("ec")) { // ec = Electrical Conductance
|
||||
return "µS";
|
||||
}
|
||||
return "µs";
|
||||
|
||||
case "mS/cm":
|
||||
case "ms/cm": // Siemens or seconds? ms/cm seems unlikely...
|
||||
return "mS/cm";
|
||||
|
||||
case "uS/cm":
|
||||
case "us/cm": // Siemens or seconds? µs/cm seems unlikely...
|
||||
return "µS/cm";
|
||||
|
||||
case "v":
|
||||
case "VAC":
|
||||
case "VDC":
|
||||
return "V";
|
||||
|
||||
case "w":
|
||||
case "Watt":
|
||||
return "W";
|
||||
|
||||
case "wh":
|
||||
return "Wh";
|
||||
|
||||
case "欧姆":
|
||||
return "Ω";
|
||||
|
||||
case "度": // degree/time/counter for ...
|
||||
switch (code) {
|
||||
case "KCWD":
|
||||
case "JLWD":
|
||||
case "SSWD": // window open/comfort/econ temperatures with min=5.0 max=30.0
|
||||
return "°C";
|
||||
}
|
||||
break;
|
||||
|
||||
case "分": // point/minute/fraction/part/tenth
|
||||
switch (code) {
|
||||
case "alarm_sound_duration":
|
||||
case "Clean_time":
|
||||
case "clean_time":
|
||||
return "min";
|
||||
}
|
||||
break;
|
||||
|
||||
case "档": // files/pigeonhole
|
||||
switch (code) {
|
||||
case "fan_speed":
|
||||
return "rpm";
|
||||
|
||||
case "sensitivity":
|
||||
case "move_sensitivity":
|
||||
case "presence_sensitivity":
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Not all standard functions have a default unit set and not all developers set one.
|
||||
// And not all units set are... well... sane.
|
||||
switch (code) {
|
||||
case "a_batterypencent":
|
||||
return "%";
|
||||
|
||||
case "a_BatteryVoltage":
|
||||
return "V";
|
||||
|
||||
case "add_ele":
|
||||
if (properties.scale == 3) {
|
||||
return "kWh";
|
||||
} else if (properties.scale == 0) {
|
||||
return "Wh";
|
||||
}
|
||||
break;
|
||||
|
||||
case "countdown":
|
||||
if (properties.min == 0 && properties.max == 86400) {
|
||||
return "s";
|
||||
}
|
||||
break;
|
||||
|
||||
case "time_zone":
|
||||
// A couple of products say unit="z" but have a range of 0-23. Mind you, no negative
|
||||
// offset and scale=0, step=1 so it has restricted utility anyway.
|
||||
if (properties.min == 0 && properties.max == 23) {
|
||||
return "h";
|
||||
} else {
|
||||
switch (unit) {
|
||||
case "z":
|
||||
return "";
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "wind_dir360":
|
||||
if (properties.min == 0 && (properties.max == 360 || properties.max == 361)) {
|
||||
return "°";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (unit !== "") {
|
||||
console.log("Unsure about unit " + orig_unit + " (mapped \"" + unit +"\") for " + orig_code + " in " + productKey
|
||||
+ " min=" + properties.min
|
||||
+ " max=" + properties.max
|
||||
+ " scale=" + properties.scale
|
||||
+ " step=" + properties.step
|
||||
);
|
||||
} else if (false) {
|
||||
console.log("No unit for " + orig_code + " in " + productKey
|
||||
+ " min=" + properties.min
|
||||
+ " max=" + properties.max
|
||||
+ " scale=" + properties.scale
|
||||
+ " step=" + properties.step
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const prevSchemas = require('../../../src/main/resources/schema.json');
|
||||
|
||||
const schemaJson = fs.createWriteStream("../../../target/in-schema.json");
|
||||
http.get("https://raw.githubusercontent.com/Apollon77/ioBroker.tuya/master/lib/schema.json", function(response) {
|
||||
response.setEncoding('utf8');
|
||||
@@ -32,6 +427,9 @@ http.get("https://raw.githubusercontent.com/Apollon77/ioBroker.tuya/master/lib/s
|
||||
let convertedSchemas = {};
|
||||
|
||||
for (productKey in knownSchemas) {
|
||||
if (process.argv[2] == 'existing' && typeof prevSchemas[productKey] == 'undefined') {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
let schema = JSON.parse(knownSchemas[productKey].schema);
|
||||
let convertedSchema = {};
|
||||
@@ -44,10 +442,25 @@ http.get("https://raw.githubusercontent.com/Apollon77/ioBroker.tuya/master/lib/s
|
||||
convertedEntry = {id: entry.id, type: entry.property.type};
|
||||
if (convertedEntry.type === 'enum') {
|
||||
convertedEntry['range'] = entry.property.range;
|
||||
}
|
||||
if (convertedEntry.type === 'value' && entry.property.min !== null && entry.property.max !== null) {
|
||||
convertedEntry['min'] = entry.property.min;
|
||||
convertedEntry['max'] = entry.property.max;
|
||||
} else if (convertedEntry.type === 'value') {
|
||||
if (entry.property.min !== null && entry.property.max !== null) {
|
||||
convertedEntry['min'] = entry.property.min;
|
||||
convertedEntry['max'] = entry.property.max;
|
||||
}
|
||||
if (typeof entry.property.scale !== 'undefined') {
|
||||
if (entry.property.scale == 1 && entry.property.min == 0 && entry.property.max == 1 && (typeof entry.property.step == 'undefined' || entry.property.step >= 1)) {
|
||||
console.log('Ignoring scale=1 when min=0, max=1, step=' + (typeof entry.property.step == 'undefined' ? 1 : entry.property.step) + ' for ' + productKey + " " + entry.code);
|
||||
} else if (entry.property.scale != 0) {
|
||||
convertedEntry['scale'] = entry.property.scale;
|
||||
}
|
||||
}
|
||||
if (typeof entry.property.unit === 'undefined') {
|
||||
entry.property.unit = "";
|
||||
}
|
||||
let unit = normalizeUnit(productKey, entry.code, entry.property, entry.property.unit);
|
||||
if (unit !== "") {
|
||||
convertedEntry['unit'] = unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
convertedSchema[entry.code] = convertedEntry;
|
||||
@@ -60,7 +473,16 @@ http.get("https://raw.githubusercontent.com/Apollon77/ioBroker.tuya/master/lib/s
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFile('../resources/schema.json', JSON.stringify(convertedSchemas, null, '\t'), (err) => {
|
||||
const replacer = (key, value) =>
|
||||
value instanceof Object && !(value instanceof Array) ?
|
||||
Object.keys(value)
|
||||
.sort()
|
||||
.reduce((sorted, key) => {
|
||||
sorted[key] = value[key];
|
||||
return sorted;
|
||||
}, {}) : value;
|
||||
|
||||
fs.writeFile('../resources/schema.json', JSON.stringify(convertedSchemas, replacer, '\t'), (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user