diff --git a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/MqttChannelStateDescriptionProvider.java b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/MqttChannelStateDescriptionProvider.java index 232c23fafe8..984039b7787 100644 --- a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/MqttChannelStateDescriptionProvider.java +++ b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/MqttChannelStateDescriptionProvider.java @@ -22,7 +22,9 @@ import org.openhab.binding.mqtt.generic.internal.MqttThingHandlerFactory; import org.openhab.binding.mqtt.generic.internal.handler.GenericMQTTThingHandler; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.type.DynamicCommandDescriptionProvider; import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.openhab.core.types.CommandDescription; import org.openhab.core.types.StateDescription; import org.osgi.service.component.annotations.Component; import org.slf4j.Logger; @@ -37,11 +39,14 @@ import org.slf4j.LoggerFactory; * * @author David Graeff - Initial contribution */ -@Component(service = { DynamicStateDescriptionProvider.class, MqttChannelStateDescriptionProvider.class }) +@Component(service = { DynamicStateDescriptionProvider.class, DynamicCommandDescriptionProvider.class, + MqttChannelStateDescriptionProvider.class }) @NonNullByDefault -public class MqttChannelStateDescriptionProvider implements DynamicStateDescriptionProvider { +public class MqttChannelStateDescriptionProvider + implements DynamicStateDescriptionProvider, DynamicCommandDescriptionProvider { - private final Map descriptions = new ConcurrentHashMap<>(); + private final Map stateDescriptions = new ConcurrentHashMap<>(); + private final Map commandDescriptions = new ConcurrentHashMap<>(); private final Logger logger = LoggerFactory.getLogger(MqttChannelStateDescriptionProvider.class); /** @@ -53,33 +58,55 @@ public class MqttChannelStateDescriptionProvider implements DynamicStateDescript */ public void setDescription(ChannelUID channelUID, StateDescription description) { logger.debug("Adding state description for channel {}", channelUID); - descriptions.put(channelUID, description); + stateDescriptions.put(channelUID, description); + } + + /** + * Set a command description for a channel. + * A previous description, if existed, will be replaced. + * + * @param channelUID channel UID + * @param description command description for the channel + */ + public void setDescription(ChannelUID channelUID, CommandDescription description) { + logger.debug("Adding state description for channel {}", channelUID); + commandDescriptions.put(channelUID, description); } /** * Clear all registered state descriptions */ public void removeAllDescriptions() { - logger.debug("Removing all state descriptions"); - descriptions.clear(); + logger.debug("Removing all descriptions"); + stateDescriptions.clear(); + commandDescriptions.clear(); } @Override public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { - StateDescription description = descriptions.get(channel.getUID()); + StateDescription description = stateDescriptions.get(channel.getUID()); if (description != null) { logger.trace("Providing state description for channel {}", channel.getUID()); } return description; } + @Override + public @Nullable CommandDescription getCommandDescription(Channel channel, + @Nullable CommandDescription originalCommandDescription, @Nullable Locale locale) { + CommandDescription description = commandDescriptions.get(channel.getUID()); + logger.trace("Providing command description for channel {}", channel.getUID()); + return description; + } + /** - * Removes the given channel state description. + * Removes the given channel description. * * @param channel The channel */ public void remove(ChannelUID channel) { - descriptions.remove(channel); + stateDescriptions.remove(channel); + commandDescriptions.remove(channel); } } diff --git a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/generic/internal/MqttBindingConstants.java b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/generic/internal/MqttBindingConstants.java index ed09177b6ff..9e79d750549 100644 --- a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/generic/internal/MqttBindingConstants.java +++ b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/generic/internal/MqttBindingConstants.java @@ -29,7 +29,15 @@ public class MqttBindingConstants { // List of all Thing Type UIDs public static final ThingTypeUID HOMIE300_MQTT_THING = new ThingTypeUID(BINDING_ID, "homie300"); - public static final String CONFIG_HOMIE_CHANNEL = "channel-type:mqtt:homie-channel"; + public static final String CHANNEL_TYPE_HOMIE_PREFIX = "homie-"; + public static final String CHANNEL_TYPE_HOMIE_STRING = "homie-string"; + public static final String CHANNEL_TYPE_HOMIE_TRIGGER = "homie-trigger"; + + public static final String CHANNEL_PROPERTY_DATATYPE = "datatype"; + public static final String CHANNEL_PROPERTY_SETTABLE = "settable"; + public static final String CHANNEL_PROPERTY_RETAINED = "retained"; + public static final String CHANNEL_PROPERTY_FORMAT = "format"; + public static final String CHANNEL_PROPERTY_UNIT = "unit"; public static final String HOMIE_PROPERTY_VERSION = "homieversion"; public static final String HOMIE_PROPERTY_HEARTBEAT_INTERVAL = "heartbeat_interval"; diff --git a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/generic/internal/MqttThingHandlerFactory.java b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/generic/internal/MqttThingHandlerFactory.java index 66199c5175d..30a7a64e136 100644 --- a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/generic/internal/MqttThingHandlerFactory.java +++ b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/generic/internal/MqttThingHandlerFactory.java @@ -16,6 +16,7 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.generic.TransformationServiceProvider; import org.openhab.binding.mqtt.homie.internal.handler.HomieThingHandler; @@ -24,12 +25,11 @@ import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.transform.TransformationHelper; import org.openhab.core.transform.TransformationService; -import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; /** @@ -41,43 +41,40 @@ import org.osgi.service.component.annotations.Reference; @Component(service = ThingHandlerFactory.class) @NonNullByDefault public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements TransformationServiceProvider { - private @NonNullByDefault({}) MqttChannelTypeProvider typeProvider; + private final MqttChannelTypeProvider typeProvider; + private final MqttChannelStateDescriptionProvider stateDescriptionProvider; + private final ChannelTypeRegistry channelTypeRegistry; + + @Activate + public MqttThingHandlerFactory(final @Reference MqttChannelTypeProvider typeProvider, + final @Reference MqttChannelStateDescriptionProvider stateDescriptionProvider, + final @Reference ChannelTypeRegistry channelTypeRegistry) { + this.typeProvider = typeProvider; + this.stateDescriptionProvider = stateDescriptionProvider; + this.channelTypeRegistry = channelTypeRegistry; + } + private static final Set SUPPORTED_THING_TYPES_UIDS = Set .of(MqttBindingConstants.HOMIE300_MQTT_THING); @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) || isHomieDynamicType(thingTypeUID); } - @Activate - @Override - protected void activate(ComponentContext componentContext) { - super.activate(componentContext); - } - - @Deactivate - @Override - protected void deactivate(ComponentContext componentContext) { - super.deactivate(componentContext); - } - - @Reference - protected void setChannelProvider(MqttChannelTypeProvider provider) { - this.typeProvider = provider; - } - - protected void unsetChannelProvider(MqttChannelTypeProvider provider) { - this.typeProvider = null; + private boolean isHomieDynamicType(ThingTypeUID thingTypeUID) { + return MqttBindingConstants.BINDING_ID.equals(thingTypeUID.getBindingId()) + && thingTypeUID.getId().startsWith(MqttBindingConstants.HOMIE300_MQTT_THING.getId()); } @Override protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - if (thingTypeUID.equals(MqttBindingConstants.HOMIE300_MQTT_THING)) { - return new HomieThingHandler(thing, typeProvider, MqttBindingConstants.HOMIE_DEVICE_TIMEOUT_MS, - MqttBindingConstants.HOMIE_SUBSCRIBE_TIMEOUT_MS, MqttBindingConstants.HOMIE_ATTRIBUTE_TIMEOUT_MS); + if (supportsThingType(thingTypeUID)) { + return new HomieThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry, + MqttBindingConstants.HOMIE_DEVICE_TIMEOUT_MS, MqttBindingConstants.HOMIE_SUBSCRIBE_TIMEOUT_MS, + MqttBindingConstants.HOMIE_ATTRIBUTE_TIMEOUT_MS); } return null; } diff --git a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandler.java b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandler.java index 923038be5af..cb305be7841 100644 --- a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandler.java +++ b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandler.java @@ -13,6 +13,7 @@ package org.openhab.binding.mqtt.homie.internal.handler; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledFuture; @@ -23,6 +24,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mqtt.generic.AbstractMQTTThingHandler; import org.openhab.binding.mqtt.generic.ChannelState; +import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.generic.tools.DelayedBatchProcessing; import org.openhab.binding.mqtt.homie.generic.internal.MqttBindingConstants; @@ -39,6 +41,12 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.type.ChannelGroupDefinition; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.types.CommandDescription; +import org.openhab.core.types.StateDescription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,6 +61,8 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic private final Logger logger = LoggerFactory.getLogger(HomieThingHandler.class); protected Device device; protected final MqttChannelTypeProvider channelTypeProvider; + protected final MqttChannelStateDescriptionProvider stateDescriptionProvider; + protected final ChannelTypeRegistry channelTypeRegistry; /** The timeout per attribute field subscription */ protected final int attributeReceiveTimeout; protected final int subscribeTimeout; @@ -67,16 +77,21 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic * * @param thing The thing of this handler * @param channelTypeProvider A channel type provider + * @param stateDescriptionProvider A state description provider + * @param channelTypeRegistry The channel type registry * @param deviceTimeout Timeout for the entire device subscription. In milliseconds. * @param subscribeTimeout Timeout for an entire attribute class subscription and receive. In milliseconds. * Even a slow remote device will publish a full node or property within 100ms. * @param attributeReceiveTimeout The timeout per attribute field subscription. In milliseconds. * One attribute subscription and receiving should not take longer than 50ms. */ - public HomieThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider, int deviceTimeout, - int subscribeTimeout, int attributeReceiveTimeout) { + public HomieThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider, + MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry, + int deviceTimeout, int subscribeTimeout, int attributeReceiveTimeout) { super(thing, deviceTimeout); this.channelTypeProvider = channelTypeProvider; + this.stateDescriptionProvider = stateDescriptionProvider; + this.channelTypeRegistry = channelTypeRegistry; this.deviceTimeout = deviceTimeout; this.subscribeTimeout = subscribeTimeout; this.attributeReceiveTimeout = attributeReceiveTimeout; @@ -105,6 +120,17 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic return; } device.initialize(config.basetopic, config.deviceid, thing.getChannels()); + + updateThingType(); + if (getThing().getThingTypeUID().equals(MqttBindingConstants.HOMIE300_MQTT_THING)) { + logger.debug("Migrating Homie thing {} from generic type to dynamic type {}", getThing().getUID(), + device.thingTypeUID); + changeThingType(device.thingTypeUID, getConfig()); + return; + } else { + updateChannels(); + } + super.initialize(); } @@ -143,6 +169,7 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic } delayedProcessing.join(); device.stop(); + channelTypeProvider.removeThingType(device.thingTypeUID); super.stop(); } @@ -195,7 +222,7 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic @Override public void propertyRemoved(Property property) { - channelTypeProvider.removeChannelType(property.channelTypeUID); + stateDescriptionProvider.remove(property.getChannelUID()); delayedProcessing.accept(property); } @@ -207,7 +234,16 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic @Override public void propertyAddedOrChanged(Property property) { - channelTypeProvider.setChannelType(property.channelTypeUID, property.getType()); + ChannelUID channelUID = property.getChannelUID(); + stateDescriptionProvider.remove(channelUID); + StateDescription stateDescription = property.getStateDescription(); + if (stateDescription != null) { + stateDescriptionProvider.setDescription(channelUID, stateDescription); + } + CommandDescription commandDescription = property.getCommandDescription(); + if (commandDescription != null) { + stateDescriptionProvider.setDescription(channelUID, commandDescription); + } delayedProcessing.accept(property); } @@ -220,10 +256,9 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic if (!device.isInitialized()) { return; } - List channels = device.nodes().stream().flatMap(n -> n.properties.stream()).map(Property::getChannel) - .collect(Collectors.toList()); - updateThing(editThing().withChannels(channels).build()); updateProperty(MqttBindingConstants.HOMIE_PROPERTY_VERSION, device.attributes.homie); + updateThingType(); + updateChannels(); final MqttBrokerConnection connection = this.connection; if (connection != null) { device.startChannels(connection, scheduler, attributeReceiveTimeout, this).thenRun(() -> { @@ -249,4 +284,31 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic protected void updateThingStatus(boolean messageReceived, Optional availabilityTopicsSeen) { // not used here } + + private void updateThingType() { + // Make sure any dynamic channel types exist (i.e. ones created for a number channel with a specific dimension) + device.nodes.stream().flatMap(n -> n.properties.stream()).map(Property::getChannelType).filter(Objects::nonNull) + .forEach(ct -> channelTypeProvider.setChannelType(ct.getUID(), ct)); + + // if this is a dynamic type, then we update the type + ThingTypeUID typeID = device.thingTypeUID; + if (!MqttBindingConstants.HOMIE300_MQTT_THING.equals(typeID)) { + device.nodes.stream() + .forEach(n -> channelTypeProvider.setChannelGroupType(n.channelGroupTypeUID, n.type())); + + List groupDefs = device.nodes().stream().map(Node::getChannelGroupDefinition) + .collect(Collectors.toList()); + var builder = channelTypeProvider.derive(typeID, MqttBindingConstants.HOMIE300_MQTT_THING) + .withChannelGroupDefinitions(groupDefs); + ThingType thingType = builder.build(); + + channelTypeProvider.setThingType(typeID, thingType); + } + } + + private void updateChannels() { + List channels = device.nodes().stream().flatMap(n -> n.properties.stream()) + .map(p -> p.getChannel(channelTypeRegistry)).collect(Collectors.toList()); + updateThing(editThing().withChannels(channels).build()); + } } diff --git a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Device.java b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Device.java index a1aab167f59..47dd8a52493 100644 --- a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Device.java +++ b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Device.java @@ -22,13 +22,14 @@ import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.mqtt.generic.ChannelConfig; import org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass; import org.openhab.binding.mqtt.generic.tools.ChildMap; +import org.openhab.binding.mqtt.homie.generic.internal.MqttBindingConstants; import org.openhab.binding.mqtt.homie.internal.handler.HomieThingHandler; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.util.UIDUtils; import org.slf4j.Logger; @@ -61,6 +62,7 @@ public class Device implements AbstractMqttAttributeClass.AttributeChanged { // The corresponding ThingUID and callback of this device object public final ThingUID thingUID; + public ThingTypeUID thingTypeUID = MqttBindingConstants.HOMIE300_MQTT_THING; private final DeviceCallback callback; // Unique identifier and topic @@ -76,10 +78,7 @@ public class Device implements AbstractMqttAttributeClass.AttributeChanged { * @param attributes The device attributes object */ public Device(ThingUID thingUID, DeviceCallback callback, DeviceAttributes attributes) { - this.thingUID = thingUID; - this.callback = callback; - this.attributes = attributes; - this.nodes = new ChildMap<>(); + this(thingUID, callback, attributes, new ChildMap<>()); } /** @@ -204,13 +203,11 @@ public class Device implements AbstractMqttAttributeClass.AttributeChanged { public void initialize(String baseTopic, String deviceID, List channels) { this.topic = baseTopic + "/" + deviceID; this.deviceID = deviceID; + this.thingTypeUID = new ThingTypeUID(MqttBindingConstants.BINDING_ID, + MqttBindingConstants.HOMIE300_MQTT_THING.getId() + "_" + UIDUtils.encode(topic)); + nodes.clear(); for (Channel channel : channels) { - final ChannelConfig channelConfig = channel.getConfiguration().as(ChannelConfig.class); - if (!channelConfig.commandTopic.isEmpty() && !channelConfig.retained) { - logger.warn("Channel {} in device {} is missing the 'retained' flag. Check your configuration.", - channel.getUID(), deviceID); - } final String channelGroupId = channel.getUID().getGroupId(); if (channelGroupId == null) { continue; @@ -223,9 +220,43 @@ public class Device implements AbstractMqttAttributeClass.AttributeChanged { node.nodeRestoredFromConfig(); nodes.put(nodeID, node); } - // Restores the properties attribute object via the channels configuration. - Property property = node.createProperty(propertyID, - channel.getConfiguration().as(PropertyAttributes.class)); + // Restores the property's attributes object via the channel's config and properties. + // (config is only for backwards compatibility before properties were used) + var channelConfig = channel.getConfiguration(); + PropertyAttributes attributes = channelConfig.as(PropertyAttributes.class); + + var channelProperties = channel.getProperties(); + String channelId = channel.getChannelTypeUID().getId(); + + String datatype = channelProperties.get(MqttBindingConstants.CHANNEL_PROPERTY_DATATYPE); + if (datatype != null) { + attributes.datatype = PropertyAttributes.DataTypeEnum.valueOf(datatype); + } else if (channelId.startsWith(MqttBindingConstants.CHANNEL_TYPE_HOMIE_PREFIX)) { + attributes.datatype = PropertyAttributes.DataTypeEnum + .valueOf(channelId.substring(MqttBindingConstants.CHANNEL_TYPE_HOMIE_PREFIX.length()) + "_"); + } + String label = channel.getLabel(); + if (label != null) { + attributes.name = label; + } + String settable = channelProperties.get(MqttBindingConstants.CHANNEL_PROPERTY_SETTABLE); + if (settable != null) { + attributes.settable = Boolean.valueOf(settable); + } + String retained = channelProperties.get(MqttBindingConstants.CHANNEL_PROPERTY_RETAINED); + if (retained != null) { + attributes.retained = Boolean.valueOf(retained); + } + String unit = channelProperties.get(MqttBindingConstants.CHANNEL_PROPERTY_UNIT); + if (unit != null) { + attributes.unit = unit; + } + String format = channelProperties.get(MqttBindingConstants.CHANNEL_PROPERTY_FORMAT); + if (format != null) { + attributes.format = format; + } + + Property property = node.createProperty(propertyID, attributes); property.attributesReceived(); node.properties.put(propertyID, property); diff --git a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Node.java b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Node.java index de5bc075e4f..d9d9592250e 100644 --- a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Node.java +++ b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Node.java @@ -29,7 +29,7 @@ import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.thing.ChannelGroupUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.type.ChannelDefinition; -import org.openhab.core.thing.type.ChannelDefinitionBuilder; +import org.openhab.core.thing.type.ChannelGroupDefinition; import org.openhab.core.thing.type.ChannelGroupType; import org.openhab.core.thing.type.ChannelGroupTypeBuilder; import org.openhab.core.thing.type.ChannelGroupTypeUID; @@ -101,6 +101,7 @@ public class Node implements AbstractMqttAttributeClass.AttributeChanged { public void nodeRestoredFromConfig() { initialized = true; + attributes.name = nodeID; } /** @@ -118,12 +119,15 @@ public class Node implements AbstractMqttAttributeClass.AttributeChanged { */ public ChannelGroupType type() { final List channelDefinitions = properties.stream() - .map(c -> new ChannelDefinitionBuilder(c.propertyID, c.channelTypeUID).build()) - .collect(Collectors.toList()); + .map(p -> Objects.requireNonNull(p.getChannelDefinition())).collect(Collectors.toList()); return ChannelGroupTypeBuilder.instance(channelGroupTypeUID, attributes.name) .withChannelDefinitions(channelDefinitions).build(); } + public ChannelGroupDefinition getChannelGroupDefinition() { + return new ChannelGroupDefinition(channelGroupUID.getId(), channelGroupTypeUID, attributes.name, null); + } + /** * Return the channel group UID. */ diff --git a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Property.java b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Property.java index cb1d398f5ab..37937074758 100644 --- a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Property.java +++ b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Property.java @@ -14,14 +14,18 @@ package org.openhab.binding.mqtt.homie.internal.homie300; import java.math.BigDecimal; import java.math.MathContext; -import java.net.URI; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; 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.mqtt.generic.ChannelConfigBuilder; @@ -37,16 +41,21 @@ import org.openhab.binding.mqtt.generic.values.TextValue; import org.openhab.binding.mqtt.generic.values.Value; import org.openhab.binding.mqtt.homie.generic.internal.MqttBindingConstants; import org.openhab.binding.mqtt.homie.internal.homie300.PropertyAttributes.DataTypeEnum; -import org.openhab.core.config.core.Configuration; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.CommonTriggerEvents; import org.openhab.core.thing.DefaultSystemChannelTypeProvider; import org.openhab.core.thing.binding.builder.ChannelBuilder; import org.openhab.core.thing.type.AutoUpdatePolicy; +import org.openhab.core.thing.type.ChannelDefinition; +import org.openhab.core.thing.type.ChannelDefinitionBuilder; import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeBuilder; +import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.CommandDescription; +import org.openhab.core.types.StateDescription; import org.openhab.core.types.util.UnitUtils; import org.openhab.core.util.UIDUtils; import org.slf4j.Logger; @@ -67,9 +76,11 @@ public class Property implements AttributeChanged { // Runtime state protected @Nullable ChannelState channelState; public final ChannelUID channelUID; - public final ChannelTypeUID channelTypeUID; - private ChannelType type; - private Channel channel; + public ChannelTypeUID channelTypeUID; + private @Nullable ChannelType channelType = null; + private @Nullable ChannelDefinition channelDefinition = null; + private @Nullable StateDescription stateDescription = null; + private @Nullable CommandDescription commandDescription = null; private final String topic; private final DeviceCallback callback; protected boolean initialized = false; @@ -89,9 +100,8 @@ public class Property implements AttributeChanged { this.parentNode = node; this.propertyID = propertyID; channelUID = new ChannelUID(node.uid(), UIDUtils.encode(propertyID)); - channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID, UIDUtils.encode(this.topic)); - type = ChannelTypeBuilder.trigger(channelTypeUID, "dummy").build(); // Dummy value - channel = ChannelBuilder.create(channelUID, "dummy").build();// Dummy value + channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID, + MqttBindingConstants.CHANNEL_TYPE_HOMIE_STRING); } /** @@ -128,52 +138,11 @@ public class Property implements AttributeChanged { * ChannelState are determined. */ public void attributesReceived() { - createChannelFromAttribute(); + createChannelTypeFromAttributes(); callback.propertyAddedOrChanged(this); } - /** - * Creates the ChannelType of the Homie property. - * - * @param attributes Attributes of the property. - * @param channelState ChannelState of the property. - * - * @return Returns the ChannelType to be used to build the Channel. - */ - private ChannelType createChannelType(PropertyAttributes attributes, ChannelState channelState) { - // Retained property -> State channel - if (attributes.retained) { - return ChannelTypeBuilder.state(channelTypeUID, attributes.name, channelState.getItemType()) - .withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HOMIE_CHANNEL)) - .withStateDescriptionFragment( - channelState.getCache().createStateDescription(!attributes.settable).build()) - .build(); - } else { - // Non-retained and settable property -> State channel - if (attributes.settable) { - return ChannelTypeBuilder.state(channelTypeUID, attributes.name, channelState.getItemType()) - .withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HOMIE_CHANNEL)) - .withCommandDescription(channelState.getCache().createCommandDescription().build()) - .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); - } - // Non-retained and non settable property -> Trigger channel - if (attributes.datatype.equals(DataTypeEnum.enum_)) { - if (attributes.format.contains("PRESSED") && attributes.format.contains("RELEASED")) { - return DefaultSystemChannelTypeProvider.SYSTEM_RAWBUTTON; - } else if (attributes.format.contains("SHORT_PRESSED") && attributes.format.contains("LONG_PRESSED") - && attributes.format.contains("DOUBLE_PRESSED")) { - return DefaultSystemChannelTypeProvider.SYSTEM_BUTTON; - } else if (attributes.format.contains("DIR1_PRESSED") && attributes.format.contains("DIR1_RELEASED") - && attributes.format.contains("DIR2_PRESSED") && attributes.format.contains("DIR2_RELEASED")) { - return DefaultSystemChannelTypeProvider.SYSTEM_RAWROCKER; - } - } - return ChannelTypeBuilder.trigger(channelTypeUID, attributes.name) - .withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HOMIE_CHANNEL)).build(); - } - } - - public void createChannelFromAttribute() { + private void createChannelTypeFromAttributes() { final String commandTopic = topic + "/set"; final String stateTopic = topic; @@ -184,6 +153,12 @@ public class Property implements AttributeChanged { attributes.name = propertyID; } + Unit unit = UnitUtils.parseUnit(attributes.unit); + String dimension = null; + if (unit != null) { + dimension = UnitUtils.getDimensionName(unit); + } + switch (attributes.datatype) { case boolean_: value = new OnOffValue("true", "false"); @@ -218,7 +193,7 @@ public class Property implements AttributeChanged { if (attributes.unit.contains("%") && attributes.settable) { value = new PercentageValue(min, max, step, null, null); } else { - value = new NumberValue(min, max, step, UnitUtils.parseUnit(attributes.unit)); + value = new NumberValue(min, max, step, unit); } break; case datetime_: @@ -245,12 +220,86 @@ public class Property implements AttributeChanged { final ChannelState channelState = new ChannelState(b.build(), channelUID, value, callback); this.channelState = channelState; - final ChannelType type = createChannelType(attributes, channelState); - this.type = type; + Map channelProperties = new HashMap<>(); - this.channel = ChannelBuilder.create(channelUID, type.getItemType()).withType(type.getUID()) - .withKind(type.getKind()).withLabel(attributes.name) - .withConfiguration(new Configuration(attributes.asMap())).build(); + if (attributes.settable) { + channelProperties.put(MqttBindingConstants.CHANNEL_PROPERTY_SETTABLE, + Boolean.toString(attributes.settable)); + } + if (!attributes.retained) { + channelProperties.put(MqttBindingConstants.CHANNEL_PROPERTY_RETAINED, + Boolean.toString(attributes.retained)); + } + + if (!attributes.format.isEmpty()) { + channelProperties.put(MqttBindingConstants.CHANNEL_PROPERTY_FORMAT, attributes.format); + } + + this.channelType = null; + if (!attributes.retained && !attributes.settable) { + channelProperties.put(MqttBindingConstants.CHANNEL_PROPERTY_DATATYPE, attributes.datatype.toString()); + if (attributes.datatype.equals(DataTypeEnum.enum_)) { + if (attributes.format.contains(CommonTriggerEvents.PRESSED) + && attributes.format.contains(CommonTriggerEvents.RELEASED)) { + this.channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_RAWBUTTON; + } else if (attributes.format.contains(CommonTriggerEvents.SHORT_PRESSED) + && attributes.format.contains(CommonTriggerEvents.LONG_PRESSED) + && attributes.format.contains(CommonTriggerEvents.DOUBLE_PRESSED)) { + this.channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_BUTTON; + } else if (attributes.format.contains(CommonTriggerEvents.DIR1_PRESSED) + && attributes.format.contains(CommonTriggerEvents.DIR1_RELEASED) + && attributes.format.contains(CommonTriggerEvents.DIR2_PRESSED) + && attributes.format.contains(CommonTriggerEvents.DIR2_RELEASED)) { + this.channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_RAWROCKER; + } else { + this.channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_TRIGGER; + } + } else { + this.channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_TRIGGER; + } + } else { + if (!attributes.unit.isEmpty()) { + channelProperties.put(MqttBindingConstants.CHANNEL_PROPERTY_UNIT, attributes.unit); + } + + String channelTypeId; + + if (attributes.datatype.equals(DataTypeEnum.unknown)) { + channelTypeId = MqttBindingConstants.CHANNEL_TYPE_HOMIE_STRING; + } else if (dimension != null) { + channelTypeId = MqttBindingConstants.CHANNEL_TYPE_HOMIE_PREFIX + "number-" + dimension.toLowerCase(); + channelProperties.put(MqttBindingConstants.CHANNEL_PROPERTY_DATATYPE, attributes.datatype.toString()); + } else { + channelTypeId = MqttBindingConstants.CHANNEL_TYPE_HOMIE_PREFIX + attributes.datatype.toString(); + channelTypeId = channelTypeId.substring(0, channelTypeId.length() - 1); + } + this.channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID, channelTypeId); + if (dimension != null) { + this.channelType = ChannelTypeBuilder.state(channelTypeUID, dimension + " Value", "Number:" + dimension) + .build(); + } + + if (attributes.retained) { + this.commandDescription = null; + this.stateDescription = channelState.getCache().createStateDescription(!attributes.settable).build() + .toStateDescription(); + } else if (attributes.settable) { + this.commandDescription = channelState.getCache().createCommandDescription().build(); + this.stateDescription = null; + } else { + this.commandDescription = null; + this.stateDescription = null; + } + } + + var builder = new ChannelDefinitionBuilder(UIDUtils.encode(propertyID), channelTypeUID) + .withLabel(attributes.name).withProperties(channelProperties); + + if (attributes.settable && !attributes.retained) { + builder.withAutoUpdatePolicy(AutoUpdatePolicy.VETO); + } + + this.channelDefinition = builder.build(); } /** @@ -266,8 +315,14 @@ public class Property implements AttributeChanged { return attributes.unsubscribe(); } + public ChannelUID getChannelUID() { + return channelUID; + } + /** - * @return Returns the channelState. You should have called + * @return Returns the channelState. + * + * You should have called * {@link Property#subscribe(MqttBrokerConnection, ScheduledExecutorService, int)} * and waited for the future to complete before calling this Getter. */ @@ -275,6 +330,50 @@ public class Property implements AttributeChanged { return channelState; } + /** + * @return Returns the channelType, if a dynamic one is necessary. + * + * You should have called + * {@link Property#subscribe(AbstractMqttAttributeClass, int)} + * and waited for the future to complete before calling this Getter. + */ + public @Nullable ChannelType getChannelType() { + return channelType; + } + + /** + * @return Returns the ChannelDefinition. + * + * You should have called + * {@link Property#subscribe(AbstractMqttAttributeClass, int)} + * and waited for the future to complete before calling this Getter. + */ + public @Nullable ChannelDefinition getChannelDefinition() { + return channelDefinition; + } + + /** + * @return Returns the StateDescription. + * + * You should have called + * {@link Property#subscribe(AbstractMqttAttributeClass, int)} + * and waited for the future to complete before calling this Getter. + */ + public @Nullable StateDescription getStateDescription() { + return stateDescription; + } + + /** + * @return Returns the CommandDescription. + * + * You should have called + * {@link Property#subscribe(AbstractMqttAttributeClass, int)} + * and waited for the future to complete before calling this Getter. + */ + public @Nullable CommandDescription getCommandDescription() { + return commandDescription; + } + /** * Subscribes to the state topic on the given connection and informs about updates on the given listener. * @@ -297,19 +396,15 @@ public class Property implements AttributeChanged { } /** - * @return Returns the channel type of this property. - * The type is a dummy only if {@link #channelState} has not been set yet. + * @return Create a channel for this property. */ - public ChannelType getType() { - return type; - } + public Channel getChannel(ChannelTypeRegistry channelTypeRegistry) { + ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID); - /** - * @return Returns the channel of this property. - * The channel is a dummy only if {@link #channelState} has not been set yet. - */ - public Channel getChannel() { - return channel; + return ChannelBuilder.create(channelUID, channelType.getItemType()).withType(channelTypeUID) + .withKind(channelType.getKind()).withLabel(Objects.requireNonNull(channelDefinition.getLabel())) + .withProperties(channelDefinition.getProperties()) + .withAutoUpdatePolicy(channelDefinition.getAutoUpdatePolicy()).build(); } @Override diff --git a/bundles/org.openhab.binding.mqtt.homie/src/main/resources/OH-INF/config/homie-channel-config.xml b/bundles/org.openhab.binding.mqtt.homie/src/main/resources/OH-INF/config/homie-channel-config.xml deleted file mode 100644 index 7bb8d257527..00000000000 --- a/bundles/org.openhab.binding.mqtt.homie/src/main/resources/OH-INF/config/homie-channel-config.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - The channels unit - - - - - The channel name - - - - - Is this channel writable? - true - - - - If set to false, the resulting channel will be a trigger channel (stateless), useful for non-permanent - events. This flag corresponds to the retained option for MQTT publish. - true - - - - The output format. - - - - - The data type of this channel. - unknown - - - - - - - - - - - - diff --git a/bundles/org.openhab.binding.mqtt.homie/src/main/resources/OH-INF/thing/homie-channels.xml b/bundles/org.openhab.binding.mqtt.homie/src/main/resources/OH-INF/thing/homie-channels.xml new file mode 100644 index 00000000000..8bee833ee35 --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.homie/src/main/resources/OH-INF/thing/homie-channels.xml @@ -0,0 +1,47 @@ + + + + + Switch + + + + + Color + + + + + DateTime + + Current date and/or time + + + + String + + + + + Number + + + + + Number + + + + + String + + + + + trigger + + + diff --git a/bundles/org.openhab.binding.mqtt.homie/src/test/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandlerTests.java b/bundles/org.openhab.binding.mqtt.homie/src/test/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandlerTests.java index ac3abd24a85..09d09c6a2b2 100644 --- a/bundles/org.openhab.binding.mqtt.homie/src/test/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandlerTests.java +++ b/bundles/org.openhab.binding.mqtt.homie/src/test/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandlerTests.java @@ -41,6 +41,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.openhab.binding.mqtt.generic.ChannelState; +import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass; import org.openhab.binding.mqtt.generic.mapping.SubscribeFieldToMQTTtopic; @@ -66,9 +67,13 @@ import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.binding.ThingHandlerCallback; import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.thing.type.ThingTypeBuilder; import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.types.RefreshType; @@ -88,11 +93,14 @@ public class HomieThingHandlerTests { private @Mock @NonNullByDefault({}) ScheduledExecutorService schedulerMock; private @Mock @NonNullByDefault({}) ScheduledFuture scheduledFutureMock; private @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistryMock; + private @Mock @NonNullByDefault({}) ChannelTypeRegistry channelTypeRegistryMock; + private @Mock @NonNullByDefault({}) ChannelType channelTypeMock; private @NonNullByDefault({}) Thing thing; private @NonNullByDefault({}) HomieThingHandler thingHandler; - private final MqttChannelTypeProvider channelTypeProvider = new MqttChannelTypeProvider(thingTypeRegistryMock); + private final MqttChannelTypeProvider channelTypeProvider = spy(new MqttChannelTypeProvider(thingTypeRegistryMock)); + private final MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider(); private final String deviceID = ThingChannelConstants.TEST_HOMIE_THING.getId(); private final String deviceTopic = "homie/" + deviceID; @@ -108,8 +116,11 @@ public class HomieThingHandlerTests { config.put("basetopic", "homie"); config.put("deviceid", deviceID); - thing = ThingBuilder.create(MqttBindingConstants.HOMIE300_MQTT_THING, TEST_HOMIE_THING.getId()) - .withConfiguration(config).build(); + ThingTypeUID type = new ThingTypeUID(MqttBindingConstants.BINDING_ID, + MqttBindingConstants.HOMIE300_MQTT_THING.getId() + "_dynamic"); + doAnswer(i -> ThingTypeBuilder.instance(type, "Homie Thing")).when(channelTypeProvider).derive(any(), any()); + + thing = ThingBuilder.create(type, TEST_HOMIE_THING.getId()).withConfiguration(config).build(); thing.setStatusInfo(thingStatus); // Return the mocked connection object if the bridge handler is asked for it @@ -124,7 +135,8 @@ public class HomieThingHandlerTests { doReturn(false).when(scheduledFutureMock).isDone(); doReturn(scheduledFutureMock).when(schedulerMock).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class)); - final HomieThingHandler handler = new HomieThingHandler(thing, channelTypeProvider, 1000, 30, 5); + final HomieThingHandler handler = new HomieThingHandler(thing, channelTypeProvider, stateDescriptionProvider, + channelTypeRegistryMock, 1000, 30, 5); thingHandler = spy(handler); thingHandler.setCallback(callbackMock); final Device device = new Device(thing.getUID(), thingHandler, spy(new DeviceAttributes()), @@ -314,6 +326,10 @@ public class HomieThingHandlerTests { thingHandler.device.initialize("homie", "device", new ArrayList<>()); ThingHandlerHelper.setConnection(thingHandler, connectionMock); + doReturn("String").when(channelTypeMock).getItemType(); + doReturn(ChannelKind.STATE).when(channelTypeMock).getKind(); + doReturn(channelTypeMock).when(channelTypeRegistryMock).getChannelType(any()); + // Create mocked homie device tree with one node and one property doAnswer(this::createSubscriberAnswer).when(thingHandler.device.attributes).createSubscriber(any(), any(), any(), anyBoolean()); diff --git a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/HomieImplementationTest.java b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/HomieImplementationTest.java index 1f418ed28d2..5e325cdfa05 100644 --- a/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/HomieImplementationTest.java +++ b/itests/org.openhab.binding.mqtt.homie.tests/src/main/java/org/openhab/binding/mqtt/homie/HomieImplementationTest.java @@ -281,8 +281,8 @@ public class HomieImplementationTest extends MqttOSGiTest { assertThat(property.attributes.format, is("-100:100")); verify(property).attributesReceived(); assertNotNull(property.getChannelState()); - assertThat(property.getType().getState().getMinimum().intValue(), is(-100)); - assertThat(property.getType().getState().getMaximum().intValue(), is(100)); + assertThat(property.getStateDescription().getMinimum().intValue(), is(-100)); + assertThat(property.getStateDescription().getMaximum().intValue(), is(100)); // Check property and property attributes Property propertyBell = node.properties.get("doorbell");