[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.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<String, Object> properties) {
@Reference BasicChannelTypeProvider basicChannelTypeProvider, Map<String, Object> properties) {
this.miIoDatabaseWatchService = miIoDatabaseWatchService;
this.cloudConnector = cloudConnector;
@Nullable
@ -68,6 +70,7 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
cloudConnector.setCredentials(username, password, country);
scheduler.submit(() -> cloudConnector.isConnected());
this.channelTypeRegistry = channelTypeRegistry;
this.basicChannelTypeProvider = basicChannelTypeProvider;
}
@Override
@ -82,7 +85,8 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
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);

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.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<MiIoDeviceAction> miIoDeviceActions = new ArrayList<>();
@SerializedName("category")
@Expose
private @Nullable String category;
@SerializedName("tags")
@Expose
private @Nullable LinkedHashSet<String> tags;
@SerializedName("readmeComment")
@Expose
private @Nullable String readmeComment;
@ -167,6 +177,14 @@ public class MiIoBasicChannel {
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 class MiIoBasicChannel {
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() {
final String readmeComment = this.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.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.MiIoQuantiyTypes;
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<ChannelUID, MiIoBasicChannel> 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 class MiIoBasicHandler extends MiIoAbstractHandler {
updateData();
}, 3000, TimeUnit.MILLISECONDS);
} 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()) {
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 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
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 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
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<String> 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<String> tags = miChannel.getTags();
if (tags != null && tags.size() > 0) {
newChannel.withDefaultTags(tags);
}
}
thingBuilder.withChannel(newChannel.build());
return channelUID;