From 7202f00acd2df11cb844b110cf42c2692df4de8f Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 4 Dec 2020 13:01:18 -0800 Subject: [PATCH] [miio] dynamically generate channelTypes (#9158) * [miio] dynamically generate channelTypes Simplify the json database creation for new models and less chance for errors Related to #7276 Signed-off-by: Marcel Verpaalen Co-authored-by: Connor Petty --- .../miio/internal/MiIoHandlerFactory.java | 8 +- .../basic/BasicChannelTypeProvider.java | 120 ++++++++++++++++++ .../miio/internal/basic/MiIoBasicChannel.java | 34 +++++ .../internal/basic/OptionsValueListDTO.java | 52 ++++++++ .../internal/basic/StateDescriptionDTO.java | 110 ++++++++++++++++ .../internal/handler/MiIoBasicHandler.java | 48 ++++--- 6 files changed, 353 insertions(+), 19 deletions(-) create mode 100644 bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/BasicChannelTypeProvider.java create mode 100644 bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/OptionsValueListDTO.java create mode 100644 bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/StateDescriptionDTO.java diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java index b08b0cf23d86..45a18a2ec0e2 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoHandlerFactory.java @@ -19,6 +19,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.miio.internal.basic.BasicChannelTypeProvider; import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService; import org.openhab.binding.miio.internal.cloud.CloudConnector; import org.openhab.binding.miio.internal.handler.MiIoBasicHandler; @@ -52,11 +53,12 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory { private MiIoDatabaseWatchService miIoDatabaseWatchService; private CloudConnector cloudConnector; private ChannelTypeRegistry channelTypeRegistry; + private BasicChannelTypeProvider basicChannelTypeProvider; @Activate public MiIoHandlerFactory(@Reference ChannelTypeRegistry channelTypeRegistry, @Reference MiIoDatabaseWatchService miIoDatabaseWatchService, @Reference CloudConnector cloudConnector, - Map properties) { + @Reference BasicChannelTypeProvider basicChannelTypeProvider, Map properties) { this.miIoDatabaseWatchService = miIoDatabaseWatchService; this.cloudConnector = cloudConnector; @Nullable @@ -68,6 +70,7 @@ public MiIoHandlerFactory(@Reference ChannelTypeRegistry channelTypeRegistry, cloudConnector.setCredentials(username, password, country); scheduler.submit(() -> cloudConnector.isConnected()); this.channelTypeRegistry = channelTypeRegistry; + this.basicChannelTypeProvider = basicChannelTypeProvider; } @Override @@ -82,7 +85,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return new MiIoGenericHandler(thing, miIoDatabaseWatchService, cloudConnector); } if (thingTypeUID.equals(THING_TYPE_BASIC)) { - return new MiIoBasicHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry); + return new MiIoBasicHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry, + basicChannelTypeProvider); } if (thingTypeUID.equals(THING_TYPE_VACUUM)) { return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry); diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/BasicChannelTypeProvider.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/BasicChannelTypeProvider.java new file mode 100644 index 000000000000..edefc8a1a1f6 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/BasicChannelTypeProvider.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2010-2020 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.miio.internal.basic; + +import static org.openhab.binding.miio.internal.MiIoBindingConstants.BINDING_ID; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeBuilder; +import org.openhab.core.thing.type.ChannelTypeProvider; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.StateChannelTypeBuilder; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.openhab.core.types.StateOption; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provide channelTypes for Mi IO Basic devices + * + * @author Marcel Verpaalen - Initial contribution + */ +@Component(service = { ChannelTypeProvider.class, BasicChannelTypeProvider.class }) +@NonNullByDefault +public class BasicChannelTypeProvider implements ChannelTypeProvider { + private final Map channelTypes = new ConcurrentHashMap<>(); + private final Logger logger = LoggerFactory.getLogger(BasicChannelTypeProvider.class); + + @Override + public Collection getChannelTypes(@Nullable Locale locale) { + return channelTypes.values(); + } + + @Override + public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) { + if (channelTypes.containsKey(channelTypeUID.getAsString())) { + return channelTypes.get(channelTypeUID.getAsString()); + } + return null; + } + + public void addChannelType(MiIoBasicChannel miChannel, String model) { + ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, + model.toUpperCase().replace(".", "_") + "_" + miChannel.getChannel()); + logger.debug("Adding channel definitions for {} -> {}", channelTypeUID, miChannel.getFriendlyName()); + try { + final StateDescriptionDTO stateDescription = miChannel.getStateDescription(); + StateChannelTypeBuilder channelTypeBuilder = ChannelTypeBuilder.state(channelTypeUID, + miChannel.getFriendlyName(), miChannel.getType()); // + if (stateDescription != null) { + StateDescriptionFragmentBuilder sdf = StateDescriptionFragmentBuilder.create(); + final BigDecimal maximum = stateDescription.getMaximum(); + if (maximum != null) { + sdf.withMaximum(maximum); + } + final BigDecimal minimum = stateDescription.getMinimum(); + if (minimum != null) { + sdf.withMinimum(minimum); + } + final BigDecimal step = stateDescription.getStep(); + if (step != null) { + sdf.withStep(step); + } + final String pattern = stateDescription.getPattern(); + if (pattern != null) { + sdf.withPattern(pattern); + } + final Boolean readOnly = stateDescription.getReadOnly(); + if (readOnly != null) { + sdf.withReadOnly(readOnly); + } + List optionList = stateDescription.getOptions(); + if (optionList != null) { + List options = new ArrayList<>(); + for (OptionsValueListDTO option : optionList) { + String value = option.getValue(); + if (value != null) { + options.add(new StateOption(value, option.getLabel())); + } + } + sdf.withOptions(options); + } + channelTypeBuilder.withStateDescriptionFragment(sdf.build()); + logger.debug("added stateDescription: {}", sdf); + } + final String category = miChannel.getCategory(); + if (category != null) { + channelTypeBuilder.withCategory(category); + } + final LinkedHashSet tags = miChannel.getTags(); + if (tags != null && tags.size() > 0) { + channelTypeBuilder.withTags(tags); + } + channelTypes.put(channelTypeUID.getAsString(), channelTypeBuilder.build()); + } catch (Exception e) { + logger.warn("Failed creating channelType {}: {} ", channelTypeUID, e.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/MiIoBasicChannel.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/MiIoBasicChannel.java index 89994ec66cf6..97b7ac5e8dab 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/MiIoBasicChannel.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/MiIoBasicChannel.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -56,6 +57,9 @@ public class MiIoBasicChannel { @SerializedName("unit") @Expose private @Nullable String unit; + @SerializedName("stateDescription") + @Expose + private @Nullable StateDescriptionDTO stateDescription; @SerializedName("refresh") @Expose private @Nullable Boolean refresh; @@ -71,6 +75,12 @@ public class MiIoBasicChannel { @SerializedName("actions") @Expose private @Nullable List miIoDeviceActions = new ArrayList<>(); + @SerializedName("category") + @Expose + private @Nullable String category; + @SerializedName("tags") + @Expose + private @Nullable LinkedHashSet tags; @SerializedName("readmeComment") @Expose private @Nullable String readmeComment; @@ -167,6 +177,14 @@ public void setUnit(String unit) { this.unit = unit; } + public @Nullable StateDescriptionDTO getStateDescription() { + return stateDescription; + } + + public void setStateDescription(@Nullable StateDescriptionDTO stateDescription) { + this.stateDescription = stateDescription; + } + public Boolean getRefresh() { final @Nullable Boolean rf = refresh; return rf != null && rf.booleanValue() && !getProperty().isEmpty(); @@ -211,6 +229,22 @@ public void setTransfortmation(String transfortmation) { this.transfortmation = transfortmation; } + public @Nullable String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public @Nullable LinkedHashSet getTags() { + return tags; + } + + public void setTags(LinkedHashSet tags) { + this.tags = tags; + } + public String getReadmeComment() { final String readmeComment = this.readmeComment; return (readmeComment != null) ? readmeComment : ""; diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/OptionsValueListDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/OptionsValueListDTO.java new file mode 100644 index 000000000000..ed0cabaa4bd9 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/OptionsValueListDTO.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2020 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.miio.internal.basic; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** + * Mapping properties from json for channel options + * + * @author Marcel Verpaalen - Initial contribution + */ +@NonNullByDefault +public class OptionsValueListDTO { + + @SerializedName("value") + @Expose + public @Nullable String value; + + @SerializedName("label") + @Expose + public @Nullable String label; + + public @Nullable String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public @Nullable String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/StateDescriptionDTO.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/StateDescriptionDTO.java new file mode 100644 index 000000000000..c953d732037c --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/basic/StateDescriptionDTO.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010-2020 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.miio.internal.basic; + +import java.math.BigDecimal; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** + * Mapping properties from json for state descriptions + * + * @author Marcel Verpaalen - Initial contribution + */ +@NonNullByDefault +public class StateDescriptionDTO { + + @SerializedName("minimum") + @Expose + @Nullable + private BigDecimal minimum; + @SerializedName("maximum") + @Expose + @Nullable + private BigDecimal maximum; + @SerializedName("step") + @Expose + @Nullable + private BigDecimal step; + @SerializedName("pattern") + @Expose + @Nullable + private String pattern; + @SerializedName("readOnly") + @Expose + @Nullable + private Boolean readOnly; + @SerializedName("options") + @Expose + @Nullable + public List options = null; + + @Nullable + public BigDecimal getMinimum() { + return minimum; + } + + public void setMinimum(BigDecimal minimum) { + this.minimum = minimum; + } + + @Nullable + public BigDecimal getMaximum() { + return maximum; + } + + public void setMaximum(BigDecimal maximum) { + this.maximum = maximum; + } + + @Nullable + public BigDecimal getStep() { + return step; + } + + public void setStep(BigDecimal step) { + this.step = step; + } + + @Nullable + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + @Nullable + public Boolean getReadOnly() { + return readOnly; + } + + public void setReadOnly(Boolean readOnly) { + this.readOnly = readOnly; + } + + @Nullable + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } +} diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java index b6d12d8775d3..bffcdb8addb4 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoBasicHandler.java @@ -19,6 +19,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -34,6 +35,7 @@ import org.openhab.binding.miio.internal.MiIoSendCommand; import org.openhab.binding.miio.internal.Utils; import org.openhab.binding.miio.internal.basic.ActionConditions; +import org.openhab.binding.miio.internal.basic.BasicChannelTypeProvider; import org.openhab.binding.miio.internal.basic.CommandParameterType; import org.openhab.binding.miio.internal.basic.Conversions; import org.openhab.binding.miio.internal.basic.MiIoBasicChannel; @@ -95,11 +97,14 @@ public class MiIoBasicHandler extends MiIoAbstractHandler { private @Nullable MiIoBasicDevice miioDevice; private Map actions = new HashMap<>(); private ChannelTypeRegistry channelTypeRegistry; + private BasicChannelTypeProvider basicChannelTypeProvider; public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService, - CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) { + CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry, + BasicChannelTypeProvider basicChannelTypeProvider) { super(thing, miIoDatabaseWatchService, cloudConnector); this.channelTypeRegistry = channelTypeRegistry; + this.basicChannelTypeProvider = basicChannelTypeProvider; } @Override @@ -259,7 +264,7 @@ public void handleCommand(ChannelUID channelUID, Command receivedCommand) { updateData(); }, 3000, TimeUnit.MILLISECONDS); } else { - logger.debug("Actions not loaded yet"); + logger.debug("Actions not loaded yet, or none available"); } } @@ -409,8 +414,8 @@ private boolean buildChannelStructure(String deviceName) { for (MiIoBasicChannel miChannel : device.getDevice().getChannels()) { logger.debug("properties {}", miChannel); if (!miChannel.getType().isEmpty()) { - ChannelUID channelUID = addChannel(thingBuilder, miChannel.getChannel(), - miChannel.getChannelType(), miChannel.getType(), miChannel.getFriendlyName()); + basicChannelTypeProvider.addChannelType(miChannel, deviceName); + ChannelUID channelUID = addChannel(thingBuilder, miChannel, deviceName); if (channelUID != null) { actions.put(channelUID, miChannel); channelsAdded++; @@ -440,9 +445,10 @@ private boolean buildChannelStructure(String deviceName) { return false; } - private @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, @Nullable String channel, String channelType, - @Nullable String datatype, String friendlyName) { - if (channel == null || channel.isEmpty() || datatype == null || datatype.isEmpty()) { + private @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, MiIoBasicChannel miChannel, String model) { + String channel = miChannel.getChannel(); + String dataType = miChannel.getType(); + if (channel.isEmpty() || dataType.isEmpty()) { logger.info("Channel '{}', UID '{}' cannot be added incorrectly configured database. ", channel, getThing().getUID()); return null; @@ -455,22 +461,30 @@ private boolean buildChannelStructure(String deviceName) { logger.info("Channel '{}' for thing {} already exist... removing", channel, getThing().getUID()); thingBuilder.withoutChannel(new ChannelUID(getThing().getUID(), channel)); } - ChannelBuilder newChannel = ChannelBuilder.create(channelUID, datatype).withLabel(friendlyName); - boolean useGenericChannelType = false; - if (!channelType.isBlank()) { - ChannelTypeUID channelTypeUID = new ChannelTypeUID(channelType); + ChannelBuilder newChannel = ChannelBuilder.create(channelUID, dataType).withLabel(miChannel.getFriendlyName()); + boolean useGeneratedChannelType = false; + if (!miChannel.getChannelType().isBlank()) { + ChannelTypeUID channelTypeUID = new ChannelTypeUID(miChannel.getChannelType()); if (channelTypeRegistry.getChannelType(channelTypeUID) != null) { newChannel = newChannel.withType(channelTypeUID); + final LinkedHashSet tags = miChannel.getTags(); + if (tags != null && tags.size() > 0) { + newChannel.withDefaultTags(tags); + } } else { - logger.debug("ChannelType '{}' is not available. Check the Json file for {}", channelTypeUID, - getThing().getUID()); - useGenericChannelType = true; + logger.debug("ChannelType '{}' is not available. Check the Json file for {}", channelTypeUID, model); + useGeneratedChannelType = true; } } else { - useGenericChannelType = true; + useGeneratedChannelType = true; } - if (useGenericChannelType) { - newChannel = newChannel.withType(new ChannelTypeUID(BINDING_ID, datatype.toLowerCase())); + if (useGeneratedChannelType) { + newChannel = newChannel + .withType(new ChannelTypeUID(BINDING_ID, model.toUpperCase().replace(".", "_") + "_" + channel)); + final LinkedHashSet tags = miChannel.getTags(); + if (tags != null && tags.size() > 0) { + newChannel.withDefaultTags(tags); + } } thingBuilder.withChannel(newChannel.build()); return channelUID;