Skip to content

Commit

Permalink
[mqtt.homie] build a per-thing thing type (openhab#15893)
Browse files Browse the repository at this point in the history
* [mqtt.homie] build a per-thing thing type

Signed-off-by: Cody Cutrer <cody@cutrer.us>
Signed-off-by: Alexander Drent <Alex@Drent-ict.nl>
  • Loading branch information
ccutrer authored and adr001db committed May 12, 2024
1 parent 7d52cca commit b7d6b8c
Show file tree
Hide file tree
Showing 11 changed files with 421 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
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;
Expand All @@ -37,11 +39,14 @@
*
* @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);

/**
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

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;
Expand All @@ -24,12 +25,11 @@
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;

/**
Expand All @@ -41,43 +41,40 @@
@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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,6 +24,7 @@
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;
Expand All @@ -39,6 +41,12 @@
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;

Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -105,6 +120,17 @@ public void initialize() {
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();
}

Expand Down Expand Up @@ -143,6 +169,7 @@ protected void stop() {
}
delayedProcessing.join();
device.stop();
channelTypeProvider.removeThingType(device.thingTypeUID);
super.stop();
}

Expand Down Expand Up @@ -195,7 +222,7 @@ public void nodeRemoved(Node node) {

@Override
public void propertyRemoved(Property property) {
channelTypeProvider.removeChannelType(property.channelTypeUID);
stateDescriptionProvider.remove(property.getChannelUID());
delayedProcessing.accept(property);
}

Expand All @@ -207,7 +234,16 @@ public void nodeAddedOrChanged(Node node) {

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

Expand All @@ -220,10 +256,9 @@ public void accept(@Nullable List<Object> t) {
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(() -> {
Expand All @@ -249,4 +284,31 @@ private void removeRetainedTopics() {
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());
}
}

0 comments on commit b7d6b8c

Please sign in to comment.