Apply default semantic tags from linked channels to items (#4913)

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green
2025-09-28 20:52:56 +02:00
committed by GitHub
parent 3f9eca44ea
commit 9448de46d0
11 changed files with 560 additions and 18 deletions
@@ -31,6 +31,9 @@ public class ItemChannelLink extends AbstractLink {
private final @NonNullByDefault({}) ChannelUID channelUID;
private final Configuration configuration;
// this field tracks whether the item's current tags were applied from the linked channel's default tags
private transient boolean tagsLinked = false;
/**
* Default constructor in package scope only. Will allow to instantiate this
* class by reflection. Not intended to be used for normal instantiation.
@@ -71,4 +74,18 @@ public class ItemChannelLink extends AbstractLink {
public int hashCode() {
return super.hashCode() * configuration.hashCode();
}
/**
* Check if the item's current tags were applied from the linked channel's default tags
*/
public boolean tagsLinked() {
return tagsLinked;
}
/**
* Set the flag that indicates if the item's current tags were applied from the linked channel's default tags
*/
public void setTagsLinked(boolean value) {
tagsLinked = value;
}
}
@@ -12,20 +12,35 @@
*/
package org.openhab.core.thing.link;
import static org.openhab.core.service.StartLevelService.STARTLEVEL_COMPLETE;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.ManagedProvider;
import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.events.EventSubscriber;
import org.openhab.core.events.system.StartlevelEvent;
import org.openhab.core.items.ActiveItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemBuilderFactory;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.semantics.Point;
import org.openhab.core.semantics.Property;
import org.openhab.core.semantics.SemanticTags;
import org.openhab.core.semantics.Tag;
import org.openhab.core.service.ReadyService;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@@ -34,11 +49,16 @@ import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.UID;
import org.openhab.core.thing.link.events.LinkEventFactory;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ItemChannelLinkRegistry} tracks all {@link ItemChannelLinkProvider}s
@@ -47,20 +67,51 @@ import org.osgi.service.component.annotations.ReferencePolicy;
* @author Dennis Nobel - Initial contribution
* @author Markus Rathgeb - Linked items returns only existing items
* @author Markus Rathgeb - Rewrite collection handling to improve performance
* @author Andrew Fiddian-Green - Apply channel default tags to items
*/
@NonNullByDefault
@Component(immediate = true, service = ItemChannelLinkRegistry.class)
public class ItemChannelLinkRegistry extends AbstractLinkRegistry<ItemChannelLink, ItemChannelLinkProvider> {
@Component(immediate = true, service = ItemChannelLinkRegistry.class, configurationPid = "org.openhab.ItemChannelLinkRegistry")
public class ItemChannelLinkRegistry extends AbstractLinkRegistry<ItemChannelLink, ItemChannelLinkProvider>
implements RegistryChangeListener<Item>, EventSubscriber {
public static final String USE_TAGS = "useTags";
private final Logger logger = LoggerFactory.getLogger(ItemChannelLinkRegistry.class);
private final ThingRegistry thingRegistry;
private final ItemRegistry itemRegistry;
private final ItemBuilderFactory itemBuilderFactory;
private final ServiceRegistration<?> eventRegistration;
private boolean useTagsGlobally = false;
private int startlevel = 0;
@Activate
public ItemChannelLinkRegistry(final @Reference ThingRegistry thingRegistry,
final @Reference ItemRegistry itemRegistry) {
public ItemChannelLinkRegistry(final @Nullable Map<String, @Nullable Object> configuration,
final @Reference ThingRegistry thingRegistry, final @Reference ItemRegistry itemRegistry,
final @Reference ItemBuilderFactory itemBuilderFactory, BundleContext bundleContext) {
super(ItemChannelLinkProvider.class);
this.thingRegistry = thingRegistry;
this.itemRegistry = itemRegistry;
this.itemRegistry.addRegistryChangeListener(this);
this.itemBuilderFactory = itemBuilderFactory;
this.eventRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null);
modified(configuration);
}
@Modified
protected void modified(@Nullable Map<String, @Nullable Object> configuration) {
Object entry = configuration != null ? configuration.get(USE_TAGS) : null;
useTagsGlobally = entry != null ? Boolean.parseBoolean(entry.toString()) : false;
}
@Override
protected void deactivate() {
eventRegistration.unregister();
itemRegistry.removeRegistryChangeListener(this);
super.deactivate();
}
/**
@@ -86,8 +137,8 @@ public class ItemChannelLinkRegistry extends AbstractLinkRegistry<ItemChannelLin
* @return an unmodifiable set of bound items for the given channel UID
*/
public Set<Item> getLinkedItems(final UID uid) {
return ((Stream<Item>) super.getLinkedItemNames(uid).stream().map(itemName -> itemRegistry.get(itemName))
.filter(Objects::nonNull)).collect(Collectors.toSet());
return super.getLinkedItemNames(uid).stream().map(itemName -> itemRegistry.get(itemName))
.filter(Objects::nonNull).collect(Collectors.toSet());
}
/**
@@ -97,9 +148,8 @@ public class ItemChannelLinkRegistry extends AbstractLinkRegistry<ItemChannelLin
* @return an unmodifiable set of bound things for the given item name
*/
public Set<Thing> getBoundThings(final String itemName) {
return ((Stream<Thing>) getBoundChannels(itemName).stream()
.map(channelUID -> thingRegistry.get(channelUID.getThingUID())).filter(Objects::nonNull))
.collect(Collectors.toSet());
return getBoundChannels(itemName).stream().map(channelUID -> thingRegistry.get(channelUID.getThingUID()))
.filter(Objects::nonNull).collect(Collectors.toSet());
}
@Reference
@@ -208,18 +258,21 @@ public class ItemChannelLinkRegistry extends AbstractLinkRegistry<ItemChannelLin
@Override
protected void notifyListenersAboutAddedElement(final ItemChannelLink element) {
assignChannelDefaultTags(element);
super.notifyListenersAboutAddedElement(element);
postEvent(LinkEventFactory.createItemChannelLinkAddedEvent(element));
}
@Override
protected void notifyListenersAboutRemovedElement(final ItemChannelLink element) {
removeChannelDefaultTags(element);
super.notifyListenersAboutRemovedElement(element);
postEvent(LinkEventFactory.createItemChannelLinkRemovedEvent(element));
}
@Override
protected void notifyListenersAboutUpdatedElement(final ItemChannelLink oldElement, final ItemChannelLink element) {
assignChannelDefaultTags(element);
super.notifyListenersAboutUpdatedElement(oldElement, element);
// it is not needed to send an event, because links can not be updated
}
@@ -229,4 +282,214 @@ public class ItemChannelLinkRegistry extends AbstractLinkRegistry<ItemChannelLin
ITEM_MISSING,
ITEM_AND_THING_CHANNEL_MISSING
}
/**
* If the item does not already have a Point and/or a Property tag and if the linked channel has
* 'useTags=true' then assign the default tags from that channel to the respective item. By contrast
* if the item does already have a Point and/or a Property tag then we write a warning message in
* the log. The warning is also logged if the item has more than one linked channel with 'useTags=true'.
* And in any case if the item has native custom tags then those tags remain.
*/
private void assignChannelDefaultTags(ItemChannelLink link, ActiveItem activeItem) {
boolean alreadyHasPointOrPropertySemanticTag = false;
for (String tag : activeItem.getTags()) {
Class<? extends Tag> type = SemanticTags.getById(tag);
if ((type != null) && (Point.class.isAssignableFrom(type) || Property.class.isAssignableFrom(type))) {
alreadyHasPointOrPropertySemanticTag = true;
break;
}
}
Set<String> channelDefaultTags = getChannelDefaultTags(link);
if (!channelDefaultTags.isEmpty()) {
if (alreadyHasPointOrPropertySemanticTag) {
logger.debug("Item '{}' already tagged; ignoring tags supplied by channel '{}'.", activeItem.getName(),
link.getLinkedUID());
} else {
Set<String> newTags = new HashSet<>(activeItem.getTags());
newTags.addAll(channelDefaultTags);
logger.info("Item '{}' adding tags '{}' supplied by channel '{}'.", activeItem.getName(),
channelDefaultTags, link.getLinkedUID());
link.setTagsLinked(true);
updateExistingRegistryItemTagsAndNotifyRegistryListeners(activeItem.getName(), newTags);
}
}
}
private void assignChannelDefaultTags(ItemChannelLink link) {
try {
if (itemRegistry.getItem(link.getItemName()) instanceof ActiveItem activeItem) {
assignChannelDefaultTags(link, activeItem);
}
} catch (ItemNotFoundException e) {
// we don't expect this exception but log it anyway
logger.debug("Item '{}' not found when assigning channel default tags.", link.getItemName());
}
}
/**
* if the linked channel is the actual source of the item's tags then remove those tags from
* the item. If the item had any native custom tags they shall NOT be removed. Finally iterate
* over any other linked channels so they may eventually provide new tags.
*/
private void removeChannelDefaultTags(ItemChannelLink oldLink, ActiveItem activeItem) {
if (oldLink.tagsLinked()) {
Set<String> newTags = new HashSet<>(activeItem.getTags());
// remove old link's tags
Set<String> oldLinkTags = getChannelDefaultTags(oldLink);
newTags.removeAll(oldLinkTags);
// on openHAB shutdown tagsLinked may be true but oldLinkTags is already empty so do not log
if (startlevel >= STARTLEVEL_COMPLETE) {
logger.info("Item '{}' removing tags '{}' supplied by channel '{}'.", activeItem.getName(), oldLinkTags,
oldLink.getLinkedUID());
}
// iterate over other links in case one may assign new tags
boolean alreadyHasPointOrPropertyTag = false;
for (ItemChannelLink otherLink : getLinks(activeItem.getName())) {
if (!otherLink.getUID().equals(oldLink.getUID())) {
Set<String> otherLinkTags = getChannelDefaultTags(otherLink);
if (!otherLinkTags.isEmpty()) {
if (alreadyHasPointOrPropertyTag) {
logger.debug("Item '{}' already tagged; ignoring tags supplied by channel '{}'.",
activeItem.getName(), otherLink.getLinkedUID());
break;
} else {
alreadyHasPointOrPropertyTag = true;
newTags.addAll(otherLinkTags);
logger.info("Item '{}' adding tags '{}' supplied by channel '{}'.", activeItem.getName(),
otherLinkTags, otherLink.getLinkedUID());
otherLink.setTagsLinked(true);
}
}
}
}
oldLink.setTagsLinked(false);
updateExistingRegistryItemTagsAndNotifyRegistryListeners(activeItem.getName(), newTags);
}
}
private void removeChannelDefaultTags(ItemChannelLink link) {
try {
if (itemRegistry.getItem(link.getItemName()) instanceof ActiveItem activeItem) {
removeChannelDefaultTags(link, activeItem);
}
} catch (ItemNotFoundException e) {
// we don't expect this exception but log it anyway
logger.debug("Item '{}' not found when assigning channel default tags.", link.getItemName());
}
}
/**
* Determine if we shall get the default tags from the linked channel. This depends on the global setting 'useTags'
* and the per-link setting 'useTags'.
*
* @param link the ItemChannelLink to be used
* @return the default tags from the linked channel or an empty set
*/
private Set<String> getChannelDefaultTags(ItemChannelLink link) {
Configuration configuration = link.getConfiguration();
Object entry = configuration.get(USE_TAGS);
Boolean useTagsPerLink = entry != null ? Boolean.parseBoolean(entry.toString()) : null;
boolean getChannelTags = useTagsGlobally ? !Boolean.FALSE.equals(useTagsPerLink)
: Boolean.TRUE.equals(useTagsPerLink);
if (getChannelTags) {
ChannelUID channelUID = link.getLinkedUID();
Thing thing = thingRegistry.get(channelUID.getThingUID());
if (thing != null) {
Channel channel = thing.getChannel(channelUID.getId());
if (channel != null) {
return channel.getDefaultTags();
}
}
}
return Set.of();
}
/**
* Update the tags on the item instance in item registry and notify the item registry listeners about
* the change. For items provisioned by an .items file, the item registry is read only so instead of
* trying to replace the item with a new instance, we just modify the tags on the existing item instance.
* However since the notification method requires both old and new items so we create a 'fake old item'
* instance having the prior tags.
*
* @param itemName the name of the item in the registry that shall be be updated.
* @param newTags the new tags that shall be applied to that item instance.
*/
private void updateExistingRegistryItemTagsAndNotifyRegistryListeners(String itemName, Set<String> newTags) {
try {
if (itemRegistry.getItem(itemName) instanceof ActiveItem item) {
Item fakeOldItem = itemBuilderFactory.newItemBuilder(item).build();
item.removeAllTags();
item.addTags(newTags);
itemRegistry.notifyListenersAboutItemExternalUpdate(fakeOldItem, item);
}
} catch (ItemNotFoundException e) {
// we don't expect this exception but log it anyway
logger.debug("Item '{}' not found when assigning channel default tags.", itemName);
}
}
/**
* If a new item is added to the item registry and we already have prior "pre- orphaned"
* links to it, then update the tags from the default tags of the prior linked channels.
*/
@Override
public void added(Item item) {
if (item instanceof ActiveItem activeItem) {
getLinks(activeItem.getName()).forEach(link -> assignChannelDefaultTags(link, activeItem));
}
}
/**
* If the item has gone then clear the 'tagsLinked' flag.
*/
@Override
public void removed(Item item) {
if (item instanceof ActiveItem activeItem) {
getLinks(activeItem.getName()).forEach(link -> link.setTagsLinked(false));
}
}
/**
* Either this class applied channel default tags to the item, or something else updated the
* item (including possibly updating its tags), so in any case do NOT try to re-apply the
* channel default tags. Since that might cause an infinite loop.
*/
@Override
public void updated(Item oldItem, Item item) {
// do nothing
}
/**
* Subscribe to system start level events.
*/
@Override
public Set<String> getSubscribedEventTypes() {
return Set.of(StartlevelEvent.TYPE);
}
/**
* Re-initialize (one off) all the channel default tags when startup is complete.
*/
@Override
public void receive(Event event) {
if (event instanceof StartlevelEvent startlevelEvent) {
int newStartlevel = startlevelEvent.getStartlevel();
if (newStartlevel != startlevel && newStartlevel >= STARTLEVEL_COMPLETE) {
for (ItemChannelLink link : getAll()) {
assignChannelDefaultTags(link);
}
}
startlevel = newStartlevel;
}
}
}
@@ -464,4 +464,9 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
metadataAwareItem.updatedMetadata(oldElement, element);
}
}
@Override
public void notifyListenersAboutItemExternalUpdate(Item oldItem, Item newItem) {
notifyListenersAboutUpdatedElement(oldItem, newItem);
}
}
@@ -105,4 +105,11 @@ public interface ItemRegistry extends Registry<Item, String> {
*/
@Nullable
Item remove(String itemName, boolean recursive);
/**
* Called when an item instance has been externally updated in order to inform all registry
* listeners the change between its old and new state.
*/
default void notifyListenersAboutItemExternalUpdate(Item oldItem, Item newItem) {
}
}
@@ -130,4 +130,7 @@ Fragment-Host: org.openhab.core.model.item
com.fasterxml.jackson.dataformat.jackson-dataformat-xml;version='[2.19.2,2.19.3)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.19.2,2.19.3)',\
org.yaml.snakeyaml;version='[2.4.0,2.4.1)',\
org.openhab.core.model.persistence.runtime;version='[5.1.0,5.1.1)',\
de.focus_shift.jollyday-core;version='[1.5.4,1.5.5)',\
de.focus_shift.jollyday-jackson;version='[1.5.4,1.5.5)'
org.osgi.service.cm;version='[1.6.1,1.6.2)'
@@ -132,4 +132,5 @@ Fragment-Host: org.openhab.core.model.rule.runtime
com.fasterxml.jackson.dataformat.jackson-dataformat-xml;version='[2.19.2,2.19.3)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.19.2,2.19.3)',\
org.yaml.snakeyaml;version='[2.4.0,2.4.1)',\
org.openhab.core.model.persistence.runtime;version='[5.1.0,5.1.1)',\
org.osgi.service.cm;version='[1.6.1,1.6.2)'
@@ -135,4 +135,7 @@ Fragment-Host: org.openhab.core.model.script
com.fasterxml.jackson.core.jackson-databind;version='[2.19.2,2.19.3)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-xml;version='[2.19.2,2.19.3)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.19.2,2.19.3)',\
org.yaml.snakeyaml;version='[2.4.0,2.4.1)'
org.yaml.snakeyaml;version='[2.4.0,2.4.1)',\
org.openhab.core.model.persistence.runtime;version='[5.1.0,5.1.1)',\
de.focus_shift.jollyday-core;version='[1.5.4,1.5.5)',\
de.focus_shift.jollyday-jackson;version='[1.5.4,1.5.5)'
@@ -137,4 +137,7 @@ Fragment-Host: org.openhab.core.model.thing
com.fasterxml.jackson.dataformat.jackson-dataformat-xml;version='[2.19.2,2.19.3)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.19.2,2.19.3)',\
org.yaml.snakeyaml;version='[2.4.0,2.4.1)',\
org.openhab.core.model.persistence.runtime;version='[5.1.0,5.1.1)',\
de.focus_shift.jollyday-core;version='[1.5.4,1.5.5)',\
de.focus_shift.jollyday-jackson;version='[1.5.4,1.5.5)'
org.osgi.service.cm;version='[1.6.1,1.6.2)'
@@ -73,6 +73,65 @@ Fragment-Host: org.openhab.core.thing
org.openhab.core.thing.tests;version='[5.1.0,5.1.1)',\
org.openhab.core.transform;version='[5.1.0,5.1.1)',\
biz.aQute.tester.junit-platform;version='[7.1.0,7.1.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.3.0,2.3.1)',\
org.openhab.core.semantics;version='[5.1.0,5.1.1)',\
org.objectweb.asm;version='[9.8.0,9.8.1)'
com.google.inject;version='[7.0.0,7.0.1)',\
de.focus_shift.jollyday-core;version='[1.5.4,1.5.5)',\
de.focus_shift.jollyday-jackson;version='[1.5.4,1.5.5)',\
io.github.classgraph.classgraph;version='[4.8.180,4.8.181)',\
org.antlr.runtime;version='[3.2.0,3.2.1)',\
org.eclipse.jetty.alpn.client;version='[9.4.57,9.4.58)',\
org.eclipse.jetty.client;version='[9.4.57,9.4.58)',\
org.eclipse.jetty.http2.client;version='[9.4.57,9.4.58)',\
org.eclipse.jetty.http2.common;version='[9.4.57,9.4.58)',\
org.eclipse.jetty.http2.hpack;version='[9.4.57,9.4.58)',\
org.eclipse.jetty.jaas;version='[9.4.57,9.4.58)',\
org.eclipse.jetty.websocket.api;version='[9.4.57,9.4.58)',\
org.eclipse.jetty.websocket.client;version='[9.4.57,9.4.58)',\
org.eclipse.jetty.websocket.common;version='[9.4.57,9.4.58)',\
org.eclipse.jetty.xml;version='[9.4.57,9.4.58)',\
org.glassfish.hk2.external.aopalliance-repackaged;version='[3.1.1,3.1.2)',\
org.openhab.core.audio;version='[5.1.0,5.1.1)',\
org.openhab.core.automation;version='[5.1.0,5.1.1)',\
org.openhab.core.automation.module.script;version='[5.1.0,5.1.1)',\
org.openhab.core.automation.module.script.rulesupport;version='[5.1.0,5.1.1)',\
org.openhab.core.ephemeris;version='[5.1.0,5.1.1)',\
org.openhab.core.io.net;version='[5.1.0,5.1.1)',\
org.openhab.core.model.core;version='[5.1.0,5.1.1)',\
org.openhab.core.model.item;version='[5.1.0,5.1.1)',\
org.openhab.core.model.persistence;version='[5.1.0,5.1.1)',\
org.openhab.core.model.persistence.runtime;version='[5.1.0,5.1.1)',\
org.openhab.core.model.rule;version='[5.1.0,5.1.1)',\
org.openhab.core.model.script;version='[5.1.0,5.1.1)',\
org.openhab.core.model.script.runtime;version='[5.1.0,5.1.1)',\
org.openhab.core.model.sitemap;version='[5.1.0,5.1.1)',\
org.openhab.core.model.thing;version='[5.1.0,5.1.1)',\
org.openhab.core.persistence;version='[5.1.0,5.1.1)',\
org.openhab.core.voice;version='[5.1.0,5.1.1)',\
org.threeten.extra;version='[1.8.0,1.8.1)',\
stax2-api;version='[4.2.2,4.2.3)',\
com.fasterxml.jackson.core.jackson-annotations;version='[2.19.2,2.19.3)',\
com.fasterxml.jackson.core.jackson-core;version='[2.19.2,2.19.3)',\
com.fasterxml.jackson.core.jackson-databind;version='[2.19.2,2.19.3)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-xml;version='[2.19.2,2.19.3)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.19.2,2.19.3)',\
com.google.guava;version='[33.4.8,33.4.9)',\
com.google.guava.failureaccess;version='[1.0.3,1.0.4)',\
org.eclipse.emf.common;version='[2.30.0,2.30.1)',\
org.eclipse.emf.ecore;version='[2.36.0,2.36.1)',\
org.eclipse.emf.ecore.xmi;version='[2.37.0,2.37.1)',\
org.eclipse.equinox.common;version='[3.19.0,3.19.1)',\
org.eclipse.xtend.lib;version='[2.40.0,2.40.1)',\
org.eclipse.xtend.lib.macro;version='[2.40.0,2.40.1)',\
org.eclipse.xtext;version='[2.40.0,2.40.1)',\
org.eclipse.xtext.common.types;version='[2.40.0,2.40.1)',\
org.eclipse.xtext.util;version='[2.40.0,2.40.1)',\
org.eclipse.xtext.xbase;version='[2.40.0,2.40.1)',\
org.eclipse.xtext.xbase.lib;version='[2.40.0,2.40.1)',\
org.objectweb.asm;version='[9.8.0,9.8.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.3.0,2.3.1)',\
org.ops4j.pax.web.pax-web-api;version='[8.0.33,8.0.34)',\
org.ops4j.pax.web.pax-web-jetty;version='[8.0.33,8.0.34)',\
org.ops4j.pax.web.pax-web-runtime;version='[8.0.33,8.0.34)',\
org.ops4j.pax.web.pax-web-spi;version='[8.0.33,8.0.34)',\
org.ops4j.pax.web.pax-web-tomcat-common;version='[8.0.33,8.0.34)',\
org.yaml.snakeyaml;version='[2.4.0,2.4.1)'
@@ -34,6 +34,7 @@ import org.openhab.core.common.registry.ProviderChangeListener;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemBuilderFactory;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.ItemStateConverter;
import org.openhab.core.items.Metadata;
@@ -78,6 +79,7 @@ import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.TimeSeries;
import org.osgi.framework.BundleContext;
/**
*
@@ -89,8 +91,9 @@ import org.openhab.core.types.TimeSeries;
public class CommunicationManagerOSGiTest extends JavaOSGiTest {
private static class ItemChannelLinkRegistryAdvanced extends ItemChannelLinkRegistry {
public ItemChannelLinkRegistryAdvanced(ThingRegistry thingRegistry, ItemRegistry itemRegistry) {
super(thingRegistry, itemRegistry);
public ItemChannelLinkRegistryAdvanced(ThingRegistry thingRegistry, ItemRegistry itemRegistry,
ItemBuilderFactory itemBuilderFactory, BundleContext bundleContext) {
super(null, thingRegistry, itemRegistry, itemBuilderFactory, bundleContext);
}
@Override
@@ -152,15 +155,19 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
private @Mock @NonNullByDefault({}) ThingHandler thingHandlerMock;
private @Mock @NonNullByDefault({}) ThingRegistry thingRegistryMock;
private @Mock @NonNullByDefault({}) TriggerProfile triggerProfileMock;
private @Mock @NonNullByDefault({}) ItemBuilderFactory itemBuilderFactoryMock;
private @NonNullByDefault({}) CommunicationManager manager;
private @NonNullByDefault({}) SafeCaller safeCaller;
private ItemChannelLinkRegistryAdvanced iclRegistry = new ItemChannelLinkRegistryAdvanced(thingRegistryMock,
itemRegistryMock);
private @NonNullByDefault({}) ItemChannelLinkRegistryAdvanced iclRegistry;
@BeforeEach
public void beforeEach() {
registerVolatileStorageService();
iclRegistry = new ItemChannelLinkRegistryAdvanced(thingRegistryMock, itemRegistryMock, itemBuilderFactoryMock,
bundleContext);
when(UNIT_PROVIDER_MOCK.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
item5 = new NumberItem("Number:Temperature", ITEM_NAME_5, UNIT_PROVIDER_MOCK);
@@ -0,0 +1,174 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.thing.link;
import static org.junit.jupiter.api.Assertions.*;
import java.io.ByteArrayInputStream;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.model.ItemsStandaloneSetup;
import org.openhab.core.model.core.ModelRepository;
import org.openhab.core.model.thing.ThingStandaloneSetup;
import org.openhab.core.test.java.JavaOSGiTest;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
/**
* Tests for {@link ItemChannelLinkRegistry} tagging.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class ItemChannelLinkTaggingOSGiTest extends JavaOSGiTest {
private static final String ITEMS_MODEL_ID = "test.items";
private static final String ITEMS_MODEL = """
String Item_01 {channel="hue:device:dummy:color" }
String Item_02 {channel="hue:device:dummy:color" [useTags=false] }
String Item_03 {channel="hue:device:dummy:color" [useTags="false"] }
String Item_04 "Control, Color" {channel="hue:device:dummy:color" [useTags=true] }
String Item_05 "Control, Color" {channel="hue:device:dummy:color" [useTags="true"] }
String Item_06 "Control Color, Custom" ["Custom"] {channel="hue:device:dummy:color" [useTags=true] }
String Item_07 "'Control', Power, Custom" ["Power", "Custom"] {channel="hue:device:dummy:color" }
String Item_08 "'Control', Power, Custom" ["Power", "Custom"] {channel="hue:device:dummy:color" [useTags=true] }
String Item_09 "Switch, Custom" ["Switch", "Custom"] {channel="hue:device:dummy:color" }
String Item_10 "Switch, Custom" ["Switch", "Custom"] {channel="hue:device:dummy:color" [useTags=true] }
String Item_11 "Switch, Power, Custom" ["Switch", "Power", "Custom"] {channel="hue:device:dummy:color" }
String Item_12 "Switch, Power, Custom" ["Switch", "Power", "Custom"] {channel="hue:device:dummy:color" [useTags=true] }
String Item_13 "Alarm, LowBattery" {channel="hue:device:dummy:battery-low" [useTags=true] }
String Item_14 "Control, Color" {channel="hue:device:dummy:color" [useTags=true], channel="hue:device:dummy:battery-low" [useTags=true] }
String Item_15 "Alarm, LowBattery" {channel="hue:device:dummy:color" [useTags=false], channel="hue:device:dummy:battery-low" [useTags=true] }
""";
private @NonNullByDefault({}) ItemRegistry itemRegistry;
private @NonNullByDefault({}) ThingRegistry thingRegistry;
private @NonNullByDefault({}) ModelRepository modelRepository;
private @NonNullByDefault({}) ItemChannelLinkRegistry itemChannelLinkRegistry;
@BeforeEach
public void setup() {
registerVolatileStorageService();
ItemsStandaloneSetup.doSetup();
ThingStandaloneSetup.doSetup();
itemRegistry = getService(ItemRegistry.class);
assertNotNull(itemRegistry);
thingRegistry = getService(ThingRegistry.class);
assertNotNull(thingRegistry);
itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class);
assertNotNull(itemChannelLinkRegistry);
modelRepository = getService(ModelRepository.class);
assertNotNull(modelRepository);
}
@Test
public void assertTagsAreCorrect() {
ThingTypeUID thingTypeUID = new ThingTypeUID("hue", "device");
ThingUID thingUID = new ThingUID(thingTypeUID, "dummy");
Channel colorChannel = ChannelBuilder.create(new ChannelUID(thingUID, "color"), "Color")
.withDefaultTags(Set.of("Control", "Color")).build();
Channel batteryChannel = ChannelBuilder.create(new ChannelUID(thingUID, "battery-low"), "Switch")
.withDefaultTags(Set.of("Alarm", "LowBattery")).build();
Thing thing = ThingBuilder.create(thingTypeUID, thingUID) //
.withChannels(List.of(colorChannel, batteryChannel)).build();
thingRegistry.add(thing);
assertEquals(1, thingRegistry.getAll().size());
modelRepository.addOrRefreshModel(ITEMS_MODEL_ID, new ByteArrayInputStream(ITEMS_MODEL.getBytes()));
assertEquals(15, itemRegistry.getAll().size());
assertEquals(17, itemChannelLinkRegistry.getAll().size());
Item item;
item = itemRegistry.get("Item_01");
assertNotNull(item);
assertTrue(item.getTags().isEmpty());
item = itemRegistry.get("Item_02");
assertNotNull(item);
assertTrue(item.getTags().isEmpty());
item = itemRegistry.get("Item_03");
assertNotNull(item);
assertTrue(item.getTags().isEmpty());
item = itemRegistry.get("Item_04");
assertNotNull(item);
assertEquals(Set.of("Control", "Color"), item.getTags());
item = itemRegistry.get("Item_05");
assertNotNull(item);
assertEquals(Set.of("Control", "Color"), item.getTags());
item = itemRegistry.get("Item_06");
assertNotNull(item);
assertEquals(Set.of("Control", "Color", "Custom"), item.getTags());
item = itemRegistry.get("Item_07");
assertNotNull(item);
assertEquals(Set.of("Power", "Custom"), item.getTags());
item = itemRegistry.get("Item_08");
assertNotNull(item);
assertEquals(Set.of("Power", "Custom"), item.getTags());
item = itemRegistry.get("Item_09");
assertNotNull(item);
assertEquals(Set.of("Switch", "Custom"), item.getTags());
item = itemRegistry.get("Item_10");
assertNotNull(item);
assertEquals(Set.of("Switch", "Custom"), item.getTags());
item = itemRegistry.get("Item_11");
assertNotNull(item);
assertEquals(Set.of("Switch", "Power", "Custom"), item.getTags());
item = itemRegistry.get("Item_12");
assertNotNull(item);
assertEquals(Set.of("Switch", "Power", "Custom"), item.getTags());
item = itemRegistry.get("Item_13");
assertNotNull(item);
assertEquals(Set.of("Alarm", "LowBattery"), item.getTags());
item = itemRegistry.get("Item_14");
assertNotNull(item);
assertTrue(item.getTags().equals(Set.of("Control", "Color")) //
|| item.getTags().equals(Set.of("Alarm", "LowBattery")));
item = itemRegistry.get("Item_15");
assertNotNull(item);
assertEquals(Set.of("Alarm", "LowBattery"), item.getTags());
}
}