[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 <marcel@verpaalen.com>
Co-authored-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
Marcel 2020-12-04 13:01:18 -08:00 committed by GitHub
parent 97d9bda0a1
commit 2597ff20d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 353 additions and 19 deletions

View File

@ -19,6 +19,7 @@ import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.basic.MiIoDatabaseWatchService;
import org.openhab.binding.miio.internal.cloud.CloudConnector; import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.binding.miio.internal.handler.MiIoBasicHandler; import org.openhab.binding.miio.internal.handler.MiIoBasicHandler;
@ -52,11 +53,12 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
private MiIoDatabaseWatchService miIoDatabaseWatchService; private MiIoDatabaseWatchService miIoDatabaseWatchService;
private CloudConnector cloudConnector; private CloudConnector cloudConnector;
private ChannelTypeRegistry channelTypeRegistry; private ChannelTypeRegistry channelTypeRegistry;
private BasicChannelTypeProvider basicChannelTypeProvider;
@Activate @Activate
public MiIoHandlerFactory(@Reference ChannelTypeRegistry channelTypeRegistry, public MiIoHandlerFactory(@Reference ChannelTypeRegistry channelTypeRegistry,
@Reference MiIoDatabaseWatchService miIoDatabaseWatchService, @Reference CloudConnector cloudConnector, @Reference MiIoDatabaseWatchService miIoDatabaseWatchService, @Reference CloudConnector cloudConnector,
Map<String, Object> properties) { @Reference BasicChannelTypeProvider basicChannelTypeProvider, Map<String, Object> properties) {
this.miIoDatabaseWatchService = miIoDatabaseWatchService; this.miIoDatabaseWatchService = miIoDatabaseWatchService;
this.cloudConnector = cloudConnector; this.cloudConnector = cloudConnector;
@Nullable @Nullable
@ -68,6 +70,7 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
cloudConnector.setCredentials(username, password, country); cloudConnector.setCredentials(username, password, country);
scheduler.submit(() -> cloudConnector.isConnected()); scheduler.submit(() -> cloudConnector.isConnected());
this.channelTypeRegistry = channelTypeRegistry; this.channelTypeRegistry = channelTypeRegistry;
this.basicChannelTypeProvider = basicChannelTypeProvider;
} }
@Override @Override
@ -82,7 +85,8 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
return new MiIoGenericHandler(thing, miIoDatabaseWatchService, cloudConnector); return new MiIoGenericHandler(thing, miIoDatabaseWatchService, cloudConnector);
} }
if (thingTypeUID.equals(THING_TYPE_BASIC)) { 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)) { if (thingTypeUID.equals(THING_TYPE_VACUUM)) {
return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry); return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry);

View File

@ -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<String, ChannelType> channelTypes = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(BasicChannelTypeProvider.class);
@Override
public Collection<ChannelType> 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<OptionsValueListDTO> optionList = stateDescription.getOptions();
if (optionList != null) {
List<StateOption> 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<String> 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());
}
}
}

View File

@ -16,6 +16,7 @@ import static org.openhab.binding.miio.internal.MiIoBindingConstants.BINDING_ID;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@ -56,6 +57,9 @@ public class MiIoBasicChannel {
@SerializedName("unit") @SerializedName("unit")
@Expose @Expose
private @Nullable String unit; private @Nullable String unit;
@SerializedName("stateDescription")
@Expose
private @Nullable StateDescriptionDTO stateDescription;
@SerializedName("refresh") @SerializedName("refresh")
@Expose @Expose
private @Nullable Boolean refresh; private @Nullable Boolean refresh;
@ -71,6 +75,12 @@ public class MiIoBasicChannel {
@SerializedName("actions") @SerializedName("actions")
@Expose @Expose
private @Nullable List<MiIoDeviceAction> miIoDeviceActions = new ArrayList<>(); private @Nullable List<MiIoDeviceAction> miIoDeviceActions = new ArrayList<>();
@SerializedName("category")
@Expose
private @Nullable String category;
@SerializedName("tags")
@Expose
private @Nullable LinkedHashSet<String> tags;
@SerializedName("readmeComment") @SerializedName("readmeComment")
@Expose @Expose
private @Nullable String readmeComment; private @Nullable String readmeComment;
@ -167,6 +177,14 @@ public class MiIoBasicChannel {
this.unit = unit; this.unit = unit;
} }
public @Nullable StateDescriptionDTO getStateDescription() {
return stateDescription;
}
public void setStateDescription(@Nullable StateDescriptionDTO stateDescription) {
this.stateDescription = stateDescription;
}
public Boolean getRefresh() { public Boolean getRefresh() {
final @Nullable Boolean rf = refresh; final @Nullable Boolean rf = refresh;
return rf != null && rf.booleanValue() && !getProperty().isEmpty(); return rf != null && rf.booleanValue() && !getProperty().isEmpty();
@ -211,6 +229,22 @@ public class MiIoBasicChannel {
this.transfortmation = transfortmation; this.transfortmation = transfortmation;
} }
public @Nullable String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public @Nullable LinkedHashSet<String> getTags() {
return tags;
}
public void setTags(LinkedHashSet<String> tags) {
this.tags = tags;
}
public String getReadmeComment() { public String getReadmeComment() {
final String readmeComment = this.readmeComment; final String readmeComment = this.readmeComment;
return (readmeComment != null) ? readmeComment : ""; return (readmeComment != null) ? readmeComment : "";

View File

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

View File

@ -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<OptionsValueListDTO> 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<OptionsValueListDTO> getOptions() {
return options;
}
public void setOptions(List<OptionsValueListDTO> options) {
this.options = options;
}
}

View File

@ -19,6 +19,7 @@ import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -34,6 +35,7 @@ import org.openhab.binding.miio.internal.MiIoQuantiyTypes;
import org.openhab.binding.miio.internal.MiIoSendCommand; import org.openhab.binding.miio.internal.MiIoSendCommand;
import org.openhab.binding.miio.internal.Utils; import org.openhab.binding.miio.internal.Utils;
import org.openhab.binding.miio.internal.basic.ActionConditions; 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.CommandParameterType;
import org.openhab.binding.miio.internal.basic.Conversions; import org.openhab.binding.miio.internal.basic.Conversions;
import org.openhab.binding.miio.internal.basic.MiIoBasicChannel; import org.openhab.binding.miio.internal.basic.MiIoBasicChannel;
@ -95,11 +97,14 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
private @Nullable MiIoBasicDevice miioDevice; private @Nullable MiIoBasicDevice miioDevice;
private Map<ChannelUID, MiIoBasicChannel> actions = new HashMap<>(); private Map<ChannelUID, MiIoBasicChannel> actions = new HashMap<>();
private ChannelTypeRegistry channelTypeRegistry; private ChannelTypeRegistry channelTypeRegistry;
private BasicChannelTypeProvider basicChannelTypeProvider;
public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService, public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) { CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry,
BasicChannelTypeProvider basicChannelTypeProvider) {
super(thing, miIoDatabaseWatchService, cloudConnector); super(thing, miIoDatabaseWatchService, cloudConnector);
this.channelTypeRegistry = channelTypeRegistry; this.channelTypeRegistry = channelTypeRegistry;
this.basicChannelTypeProvider = basicChannelTypeProvider;
} }
@Override @Override
@ -259,7 +264,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
updateData(); updateData();
}, 3000, TimeUnit.MILLISECONDS); }, 3000, TimeUnit.MILLISECONDS);
} else { } else {
logger.debug("Actions not loaded yet"); logger.debug("Actions not loaded yet, or none available");
} }
} }
@ -409,8 +414,8 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
for (MiIoBasicChannel miChannel : device.getDevice().getChannels()) { for (MiIoBasicChannel miChannel : device.getDevice().getChannels()) {
logger.debug("properties {}", miChannel); logger.debug("properties {}", miChannel);
if (!miChannel.getType().isEmpty()) { if (!miChannel.getType().isEmpty()) {
ChannelUID channelUID = addChannel(thingBuilder, miChannel.getChannel(), basicChannelTypeProvider.addChannelType(miChannel, deviceName);
miChannel.getChannelType(), miChannel.getType(), miChannel.getFriendlyName()); ChannelUID channelUID = addChannel(thingBuilder, miChannel, deviceName);
if (channelUID != null) { if (channelUID != null) {
actions.put(channelUID, miChannel); actions.put(channelUID, miChannel);
channelsAdded++; channelsAdded++;
@ -440,9 +445,10 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return false; return false;
} }
private @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, @Nullable String channel, String channelType, private @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, MiIoBasicChannel miChannel, String model) {
@Nullable String datatype, String friendlyName) { String channel = miChannel.getChannel();
if (channel == null || channel.isEmpty() || datatype == null || datatype.isEmpty()) { String dataType = miChannel.getType();
if (channel.isEmpty() || dataType.isEmpty()) {
logger.info("Channel '{}', UID '{}' cannot be added incorrectly configured database. ", channel, logger.info("Channel '{}', UID '{}' cannot be added incorrectly configured database. ", channel,
getThing().getUID()); getThing().getUID());
return null; return null;
@ -455,22 +461,30 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
logger.info("Channel '{}' for thing {} already exist... removing", channel, getThing().getUID()); logger.info("Channel '{}' for thing {} already exist... removing", channel, getThing().getUID());
thingBuilder.withoutChannel(new ChannelUID(getThing().getUID(), channel)); thingBuilder.withoutChannel(new ChannelUID(getThing().getUID(), channel));
} }
ChannelBuilder newChannel = ChannelBuilder.create(channelUID, datatype).withLabel(friendlyName); ChannelBuilder newChannel = ChannelBuilder.create(channelUID, dataType).withLabel(miChannel.getFriendlyName());
boolean useGenericChannelType = false; boolean useGeneratedChannelType = false;
if (!channelType.isBlank()) { if (!miChannel.getChannelType().isBlank()) {
ChannelTypeUID channelTypeUID = new ChannelTypeUID(channelType); ChannelTypeUID channelTypeUID = new ChannelTypeUID(miChannel.getChannelType());
if (channelTypeRegistry.getChannelType(channelTypeUID) != null) { if (channelTypeRegistry.getChannelType(channelTypeUID) != null) {
newChannel = newChannel.withType(channelTypeUID); newChannel = newChannel.withType(channelTypeUID);
final LinkedHashSet<String> tags = miChannel.getTags();
if (tags != null && tags.size() > 0) {
newChannel.withDefaultTags(tags);
}
} else { } else {
logger.debug("ChannelType '{}' is not available. Check the Json file for {}", channelTypeUID, logger.debug("ChannelType '{}' is not available. Check the Json file for {}", channelTypeUID, model);
getThing().getUID()); useGeneratedChannelType = true;
useGenericChannelType = true;
} }
} else { } else {
useGenericChannelType = true; useGeneratedChannelType = true;
} }
if (useGenericChannelType) { if (useGeneratedChannelType) {
newChannel = newChannel.withType(new ChannelTypeUID(BINDING_ID, datatype.toLowerCase())); newChannel = newChannel
.withType(new ChannelTypeUID(BINDING_ID, model.toUpperCase().replace(".", "_") + "_" + channel));
final LinkedHashSet<String> tags = miChannel.getTags();
if (tags != null && tags.size() > 0) {
newChannel.withDefaultTags(tags);
}
} }
thingBuilder.withChannel(newChannel.build()); thingBuilder.withChannel(newChannel.build());
return channelUID; return channelUID;