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;
*
* @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<ChannelUID, StateDescription> descriptions = new ConcurrentHashMap<>();
+ private final Map<ChannelUID, StateDescription> stateDescriptions = new ConcurrentHashMap<>();
+ private final Map<ChannelUID, CommandDescription> commandDescriptions = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(MqttChannelStateDescriptionProvider.class);
/**
*/
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);
}
}
// 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";
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;
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;
/**
@Component(service = ThingHandlerFactory.class)
@NonNullByDefault
public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements TransformationServiceProvider {
- private @NonNullByDefault({}) MqttChannelTypeProvider typeProvider;
- private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set
- .of(MqttBindingConstants.HOMIE300_MQTT_THING);
-
- @Override
- public boolean supportsThingType(ThingTypeUID thingTypeUID) {
- return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
- }
+ private final MqttChannelTypeProvider typeProvider;
+ private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
+ private final ChannelTypeRegistry channelTypeRegistry;
@Activate
- @Override
- protected void activate(ComponentContext componentContext) {
- super.activate(componentContext);
+ public MqttThingHandlerFactory(final @Reference MqttChannelTypeProvider typeProvider,
+ final @Reference MqttChannelStateDescriptionProvider stateDescriptionProvider,
+ final @Reference ChannelTypeRegistry channelTypeRegistry) {
+ this.typeProvider = typeProvider;
+ this.stateDescriptionProvider = stateDescriptionProvider;
+ this.channelTypeRegistry = channelTypeRegistry;
}
- @Deactivate
- @Override
- protected void deactivate(ComponentContext componentContext) {
- super.deactivate(componentContext);
- }
+ private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set
+ .of(MqttBindingConstants.HOMIE300_MQTT_THING);
- @Reference
- protected void setChannelProvider(MqttChannelTypeProvider provider) {
- this.typeProvider = provider;
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) || isHomieDynamicType(thingTypeUID);
}
- 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;
}
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;
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;
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;
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;
*
* @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;
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();
}
}
delayedProcessing.join();
device.stop();
+ channelTypeProvider.removeThingType(device.thingTypeUID);
super.stop();
}
@Override
public void propertyRemoved(Property property) {
- channelTypeProvider.removeChannelType(property.channelTypeUID);
+ stateDescriptionProvider.remove(property.getChannelUID());
delayedProcessing.accept(property);
}
@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);
}
if (!device.isInitialized()) {
return;
}
- List<Channel> 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(() -> {
protected void updateThingStatus(boolean messageReceived, Optional<Boolean> 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<ChannelGroupDefinition> 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<Channel> channels = device.nodes().stream().flatMap(n -> n.properties.stream())
+ .map(p -> p.getChannel(channelTypeRegistry)).collect(Collectors.toList());
+ updateThing(editThing().withChannels(channels).build());
+ }
}
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;
// 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
* @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<>());
}
/**
public void initialize(String baseTopic, String deviceID, List<Channel> 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;
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);
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;
public void nodeRestoredFromConfig() {
initialized = true;
+ attributes.name = nodeID;
}
/**
*/
public ChannelGroupType type() {
final List<ChannelDefinition> 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.
*/
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;
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;
// 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;
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);
}
/**
* 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;
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");
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_:
final ChannelState channelState = new ChannelState(b.build(), channelUID, value, callback);
this.channelState = channelState;
- final ChannelType type = createChannelType(attributes, channelState);
- this.type = type;
+ Map<String, String> channelProperties = new HashMap<>();
+
+ 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.channel = ChannelBuilder.create(channelUID, type.getItemType()).withType(type.getUID())
- .withKind(type.getKind()).withLabel(attributes.name)
- .withConfiguration(new Configuration(attributes.asMap())).build();
+ this.channelDefinition = builder.build();
}
/**
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.
*/
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.
*
}
/**
- * @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
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<config-description:config-descriptions
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
- xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
-
- <config-description uri="channel-type:mqtt:homie-channel">
- <parameter name="unit" type="text">
- <label>Unit</label>
- <description>The channels unit</description>
- <default></default>
- </parameter>
- <parameter name="name" type="text">
- <label>Name</label>
- <description>The channel name</description>
- <default></default>
- </parameter>
- <parameter name="settable" type="text">
- <label>Settable</label>
- <description>Is this channel writable?</description>
- <default>true</default>
- </parameter>
- <parameter name="retained" type="text">
- <label>Retained</label>
- <description>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.</description>
- <default>true</default>
- </parameter>
- <parameter name="format" type="text">
- <label>Format</label>
- <description>The output format.</description>
- <default></default>
- </parameter>
- <parameter name="datatype" type="text">
- <label>Data Type</label>
- <description>The data type of this channel.</description>
- <default>unknown</default>
- <options>
- <option value="integer_">Integer</option>
- <option value="float_">Float</option>
- <option value="boolean_">Boolean</option>
- <option value="string_">String</option>
- <option value="enum_">Enumeration</option>
- <option value="color_">Colour</option>
- <option value="datetime_">DateTime</option>
- </options>
- </parameter>
- </config-description>
-</config-description:config-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mqtt"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <channel-type id="homie-boolean">
+ <item-type>Switch</item-type>
+ <label>Boolean</label>
+ </channel-type>
+
+ <channel-type id="homie-color">
+ <item-type>Color</item-type>
+ <label>Color Value (Red,Green,Blue)</label>
+ </channel-type>
+
+ <channel-type id="homie-datetime">
+ <item-type>DateTime</item-type>
+ <label>Date/Time Value</label>
+ <description>Current date and/or time</description>
+ </channel-type>
+
+ <channel-type id="homie-enum">
+ <item-type>String</item-type>
+ <label>Enum Value</label>
+ </channel-type>
+
+ <channel-type id="homie-integer">
+ <item-type>Number</item-type>
+ <label>Integer Value</label>
+ </channel-type>
+
+ <channel-type id="homie-float">
+ <item-type>Number</item-type>
+ <label>Float Value</label>
+ </channel-type>
+
+ <channel-type id="homie-string">
+ <item-type>String</item-type>
+ <label>Text Value</label>
+ </channel-type>
+
+ <channel-type id="homie-trigger">
+ <kind>trigger</kind>
+ <label>Trigger</label>
+ </channel-type>
+</thing:thing-descriptions>
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;
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;
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;
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
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()),
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());
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");