YAML configuration: add support for items/metadata/channel links (#4776)

* YAML configuration: add support for items/metadata/channel links

This PR adds the support of items in the YAML configuration file.
It also includes the support of items metadata and items channel links.

Related to #3666

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
pull/4820/head
lolodomo 2025-05-18 20:46:18 +02:00 committed by GitHub
parent 53ddb0c29c
commit bc62a202ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1800 additions and 8 deletions

View File

@ -160,7 +160,7 @@ public class FileFormatResource implements RESTResource {
MyItem: MyItem:
type: Switch type: Switch
label: Label label: Label
category: icon icon: icon
groups: groups:
- Group1 - Group1
- Group2 - Group2

View File

@ -40,6 +40,7 @@ import org.openhab.core.model.yaml.YamlElement;
import org.openhab.core.model.yaml.YamlElementName; import org.openhab.core.model.yaml.YamlElementName;
import org.openhab.core.model.yaml.YamlModelListener; import org.openhab.core.model.yaml.YamlModelListener;
import org.openhab.core.model.yaml.YamlModelRepository; import org.openhab.core.model.yaml.YamlModelRepository;
import org.openhab.core.model.yaml.internal.items.YamlItemDTO;
import org.openhab.core.model.yaml.internal.semantics.YamlSemanticTagDTO; import org.openhab.core.model.yaml.internal.semantics.YamlSemanticTagDTO;
import org.openhab.core.model.yaml.internal.things.YamlThingDTO; import org.openhab.core.model.yaml.internal.things.YamlThingDTO;
import org.openhab.core.service.WatchService; import org.openhab.core.service.WatchService;
@ -86,7 +87,8 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
private static final String READ_ONLY = "readOnly"; private static final String READ_ONLY = "readOnly";
private static final Set<String> KNOWN_ELEMENTS = Set.of( // private static final Set<String> KNOWN_ELEMENTS = Set.of( //
getElementName(YamlSemanticTagDTO.class), // "tags" getElementName(YamlSemanticTagDTO.class), // "tags"
getElementName(YamlThingDTO.class) // "things" getElementName(YamlThingDTO.class), // "things"
getElementName(YamlItemDTO.class) // "items"
); );
private static final String UNWANTED_EXCEPTION_TEXT = "at [Source: UNKNOWN; byte offset: #UNKNOWN] "; private static final String UNWANTED_EXCEPTION_TEXT = "at [Source: UNKNOWN; byte offset: #UNKNOWN] ";

View File

@ -0,0 +1,126 @@
/*
* 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.model.yaml.internal.items;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.common.registry.AbstractProvider;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.items.ItemProvider;
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.link.ItemChannelLink;
import org.openhab.core.thing.link.ItemChannelLinkProvider;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class serves as a provider for all item channel links that is found within YAML files.
* It is filled with content by the {@link YamlItemProvider}, which cannot itself implement the
* {@link ItemChannelLinkProvider} interface as it already implements {@link ItemProvider},
* which would lead to duplicate methods.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = { ItemChannelLinkProvider.class, YamlChannelLinkProvider.class })
public class YamlChannelLinkProvider extends AbstractProvider<ItemChannelLink> implements ItemChannelLinkProvider {
private final Logger logger = LoggerFactory.getLogger(YamlChannelLinkProvider.class);
// Map the channel links to each channel UID and then to each item name and finally to each model name
private Map<String, Map<String, Map<ChannelUID, ItemChannelLink>>> itemsChannelLinksMap = new ConcurrentHashMap<>();
@Override
public Collection<ItemChannelLink> getAll() {
return itemsChannelLinksMap.values().stream().flatMap(m -> m.values().stream())
.flatMap(m -> m.values().stream()).toList();
}
public Collection<ItemChannelLink> getAllFromModel(String modelName) {
return itemsChannelLinksMap.getOrDefault(modelName, Map.of()).values().stream()
.flatMap(m -> m.values().stream()).toList();
}
public void updateItemChannelLinks(String modelName, String itemName, Map<String, Configuration> channelLinks) {
Map<String, Map<ChannelUID, ItemChannelLink>> channelLinksMap = Objects
.requireNonNull(itemsChannelLinksMap.computeIfAbsent(modelName, k -> new ConcurrentHashMap<>()));
// Create a HashMap with an initial capacity of 2 (the default is 16) to save memory because most items have
// only one channel. A capacity of 2 is enough to avoid resizing the HashMap in most cases, whereas 1 would
// trigger a resize as soon as one element is added.
Map<ChannelUID, ItemChannelLink> links = Objects
.requireNonNull(channelLinksMap.computeIfAbsent(itemName, k -> new ConcurrentHashMap<>(2)));
Set<ChannelUID> linksToBeRemoved = new HashSet<>(links.keySet());
for (Map.Entry<String, Configuration> entry : channelLinks.entrySet()) {
String channelUID = entry.getKey();
Configuration configuration = entry.getValue();
ChannelUID channelUIDObject;
try {
channelUIDObject = new ChannelUID(channelUID);
} catch (IllegalArgumentException e) {
logger.warn("Invalid channel UID '{}' in channel link for item '{}'!", channelUID, itemName, e);
continue;
}
// Fix the configuration in case a profile is defined without any scope
if (configuration.containsKey("profile") && configuration.get("profile") instanceof String profile
&& profile.indexOf(":") == -1) {
String fullProfile = ProfileTypeUID.SYSTEM_SCOPE + ":" + profile;
configuration.put("profile", fullProfile);
logger.info(
"Profile '{}' for channel '{}' is missing the scope prefix, assuming the correct UID is '{}'. Check your configuration.",
profile, channelUID, fullProfile);
}
ItemChannelLink itemChannelLink = new ItemChannelLink(itemName, channelUIDObject, configuration);
linksToBeRemoved.remove(channelUIDObject);
ItemChannelLink oldLink = links.get(channelUIDObject);
if (oldLink == null) {
links.put(channelUIDObject, itemChannelLink);
logger.debug("notify added item channel link {}", itemChannelLink.getUID());
notifyListenersAboutAddedElement(itemChannelLink);
} else if (!YamlElementUtils.equalsConfig(configuration.getProperties(),
oldLink.getConfiguration().getProperties())) {
links.put(channelUIDObject, itemChannelLink);
logger.debug("notify updated item channel link {}", itemChannelLink.getUID());
notifyListenersAboutUpdatedElement(oldLink, itemChannelLink);
}
}
linksToBeRemoved.forEach(uid -> {
ItemChannelLink link = links.remove(uid);
if (link != null) {
logger.debug("notify removed item channel link {}", link.getUID());
notifyListenersAboutRemovedElement(link);
}
});
if (links.isEmpty()) {
channelLinksMap.remove(itemName);
}
if (channelLinksMap.isEmpty()) {
itemsChannelLinksMap.remove(modelName);
}
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.model.yaml.internal.items;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
/**
* The {@link YamlGroupDTO} is a data transfer object used to serialize the details of a group item
* in a YAML configuration file.
*
* @author Laurent Garnier - Initial contribution
*/
public class YamlGroupDTO {
private static final String DEFAULT_FUNCTION = "EQUALITY";
private static final Set<String> VALID_FUNCTIONS = Set.of("AND", "OR", "NAND", "NOR", "XOR", "COUNT", "AVG",
"MEDIAN", "SUM", "MIN", "MAX", "LATEST", "EARLIEST", DEFAULT_FUNCTION);
public String type;
public String dimension;
public String function;
public List<@NonNull String> parameters;
public YamlGroupDTO() {
}
public boolean isValid(@NonNull List<@NonNull String> errors, @NonNull List<@NonNull String> warnings) {
boolean ok = true;
if (!YamlElementUtils.isValidItemType(type)) {
errors.add("invalid value \"%s\" for \"type\" field in group".formatted(type));
ok = false;
} else if (YamlElementUtils.isNumberItemType(type)) {
if (!YamlElementUtils.isValidItemDimension(dimension)) {
errors.add("invalid value \"%s\" for \"dimension\" field in group".formatted(dimension));
ok = false;
}
} else if (dimension != null) {
warnings.add("\"dimension\" field in group ignored as type is not Number");
}
if (!VALID_FUNCTIONS.contains(getFunction())) {
errors.add("invalid value \"%s\" for \"function\" field".formatted(function));
ok = false;
}
return ok;
}
public @Nullable String getBaseType() {
return YamlElementUtils.getItemTypeWithDimension(type, dimension);
}
public String getFunction() {
return function != null ? function.toUpperCase() : DEFAULT_FUNCTION;
}
@Override
public int hashCode() {
return Objects.hash(getBaseType(), getFunction());
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
} else if (obj == null || getClass() != obj.getClass()) {
return false;
}
YamlGroupDTO other = (YamlGroupDTO) obj;
return Objects.equals(getBaseType(), other.getBaseType()) && Objects.equals(getFunction(), other.getFunction())
&& YamlElementUtils.equalsListStrings(parameters, other.parameters);
}
}

View File

@ -0,0 +1,301 @@
/*
* 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.model.yaml.internal.items;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.AbstractUID;
import org.openhab.core.items.GroupItem;
import org.openhab.core.model.yaml.YamlElement;
import org.openhab.core.model.yaml.YamlElementName;
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
/**
* The {@link YamlItemDTO} is a data transfer object used to serialize an item in a YAML configuration file.
*
* @author Laurent Garnier - Initial contribution
*/
@YamlElementName("items")
public class YamlItemDTO implements YamlElement, Cloneable {
private static final Pattern ID_PATTERN = Pattern.compile("[a-zA-Z0-9_][a-zA-Z0-9_-]*");
private static final Pattern CHANNEL_ID_PATTERN = Pattern
.compile("[a-zA-Z0-9_][a-zA-Z0-9_-]*(#[a-zA-Z0-9_][a-zA-Z0-9_-]*)?");
public String name;
public String type;
public String dimension;
public YamlGroupDTO group;
public String label;
public String icon;
public String format;
public String unit;
public Boolean autoupdate;
public List<@NonNull String> groups;
public Set<@NonNull String> tags;
public String channel;
public Map<@NonNull String, @NonNull Map<@NonNull String, @NonNull Object>> channels;
public Map<@NonNull String, @NonNull YamlMetadataDTO> metadata;
public YamlItemDTO() {
}
@Override
public @NonNull String getId() {
return name == null ? "" : name;
}
@Override
public void setId(@NonNull String id) {
name = id;
}
@Override
public YamlElement cloneWithoutId() {
YamlItemDTO copy;
try {
copy = (YamlItemDTO) super.clone();
copy.name = null;
return copy;
} catch (CloneNotSupportedException e) {
// Will never happen
return new YamlItemDTO();
}
}
@Override
public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) {
// Check that name is present
if (name == null || name.isBlank()) {
addToList(errors, "invalid item: name missing while mandatory");
return false;
}
boolean ok = true;
if (!ID_PATTERN.matcher(name).matches()) {
addToList(errors, "invalid item: name \"%s\" not matching the expected syntax %s".formatted(name,
ID_PATTERN.pattern()));
ok = false;
}
List<String> subErrors = new ArrayList<>();
List<String> subWarnings = new ArrayList<>();
if (type == null || type.isBlank()) {
addToList(errors, "invalid item \"%s\": \"type\" field missing while mandatory".formatted(name));
ok = false;
} else if (GroupItem.TYPE.equalsIgnoreCase(type)) {
if (dimension != null) {
addToList(warnings, "item \"%s\": \"dimension\" field ignored as type is Group".formatted(name));
}
if (group != null) {
ok &= group.isValid(subErrors, subWarnings);
subErrors.forEach(error -> {
addToList(errors, "invalid item \"%s\": %s".formatted(name, error));
});
subWarnings.forEach(warning -> {
addToList(warnings, "item \"%s\": %s".formatted(name, warning));
});
}
} else {
if (group != null) {
addToList(warnings, "item \"%s\": \"group\" field ignored as type is not Group".formatted(name));
}
if (!YamlElementUtils.isValidItemType(type)) {
addToList(errors, "invalid item \"%s\": invalid value \"%s\" for \"type\" field".formatted(name, type));
ok = false;
} else if (YamlElementUtils.isNumberItemType(type)) {
if (!YamlElementUtils.isValidItemDimension(dimension)) {
addToList(errors, "invalid item \"%s\": invalid value \"%s\" for \"dimension\" field"
.formatted(name, dimension));
ok = false;
}
} else if (dimension != null) {
addToList(warnings,
"item \"%s\": \"dimension\" field ignored as type is not Number".formatted(name, dimension));
}
}
if (icon != null) {
subErrors.clear();
ok &= isValidIcon(icon, subErrors);
subErrors.forEach(error -> {
addToList(errors, "invalid item \"%s\": %s".formatted(name, error));
});
}
if (groups != null) {
for (String gr : groups) {
if (!ID_PATTERN.matcher(gr).matches()) {
addToList(errors,
"invalid item \"%s\": value \"%s\" for group name not matching the expected syntax %s"
.formatted(name, gr, ID_PATTERN.pattern()));
ok = false;
}
}
}
if (channel != null) {
subErrors.clear();
ok &= isValidChannel(channel, subErrors);
subErrors.forEach(error -> {
addToList(errors, "invalid item \"%s\": %s".formatted(name, error));
});
}
if (channels != null) {
for (String ch : channels.keySet()) {
subErrors.clear();
ok &= isValidChannel(ch, subErrors);
subErrors.forEach(error -> {
addToList(errors, "invalid item \"%s\": %s".formatted(name, error));
});
}
}
if (metadata != null) {
for (String namespace : metadata.keySet()) {
if (!ID_PATTERN.matcher(namespace).matches()) {
addToList(errors, "invalid item \"%s\": metadata \"%s\" not matching the expected syntax %s"
.formatted(name, namespace, ID_PATTERN.pattern()));
ok = false;
}
}
YamlMetadataDTO md = metadata.get("autoupdate");
if (md != null && autoupdate != null) {
addToList(warnings,
"item \"%s\": \"autoupdate\" field is redundant with \"autoupdate\" metadata; value \"%s\" will be considered"
.formatted(name, md.getValue()));
}
md = metadata.get("unit");
if (md != null && unit != null) {
addToList(warnings,
"item \"%s\": \"unit\" field is redundant with \"unit\" metadata; value \"%s\" will be considered"
.formatted(name, md.getValue()));
}
md = metadata.get("stateDescription");
Map<@NonNull String, @NonNull Object> mdConfig = md == null ? null : md.config;
Object pattern = mdConfig == null ? null : mdConfig.get("pattern");
if (pattern != null && format != null) {
addToList(warnings,
"item \"%s\": \"format\" field is redundant with pattern in \"stateDescription\" metadata; \"%s\" will be considered"
.formatted(name, pattern));
}
}
return ok;
}
private boolean isValidIcon(String icon, List<@NonNull String> errors) {
boolean ok = true;
String[] segments = icon.split(AbstractUID.SEPARATOR);
int nb = segments.length;
if (nb > 3) {
errors.add("too many segments in value \"%s\" for \"icon\" field; maximum 3 is expected".formatted(icon));
ok = false;
nb = 3;
}
for (int i = 0; i < nb; i++) {
String segment = segments[i];
if (!ID_PATTERN.matcher(segment).matches()) {
errors.add("segment \"%s\" in \"icon\" field not matching the expected syntax %s".formatted(segment,
ID_PATTERN.pattern()));
ok = false;
}
}
return ok;
}
private boolean isValidChannel(String channelUID, List<@NonNull String> errors) {
boolean ok = true;
String[] segments = channelUID.split(AbstractUID.SEPARATOR);
int nb = segments.length;
if (nb < 4) {
errors.add("not enough segments in channel UID \"%s\"; minimum 4 is expected".formatted(channelUID));
ok = false;
}
String segment;
for (int i = 0; i < (nb - 1); i++) {
segment = segments[i];
if (!ID_PATTERN.matcher(segment).matches()) {
errors.add("segment \"%s\" in channel UID \"%s\" not matching the expected syntax %s".formatted(segment,
channelUID, ID_PATTERN.pattern()));
ok = false;
}
}
segment = segments[nb - 1];
if (!CHANNEL_ID_PATTERN.matcher(segment).matches()) {
errors.add("last segment \"%s\" in channel UID \"%s\" not matching the expected syntax %s"
.formatted(segment, channelUID, CHANNEL_ID_PATTERN.pattern()));
ok = false;
}
return ok;
}
private void addToList(@Nullable List<@NonNull String> list, String value) {
if (list != null) {
list.add(value);
}
}
public @Nullable String getType() {
return YamlElementUtils.getItemTypeWithDimension(type, dimension);
}
@Override
public int hashCode() {
return Objects.hash(name, getType(), label, icon);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
} else if (obj == null || getClass() != obj.getClass()) {
return false;
}
YamlItemDTO other = (YamlItemDTO) obj;
return Objects.equals(name, other.name) && Objects.equals(getType(), other.getType())
&& Objects.equals(group, other.group) && Objects.equals(label, other.label)
&& Objects.equals(icon, other.icon) && Objects.equals(format, other.format)
&& Objects.equals(unit, other.unit) && Objects.equals(autoupdate, other.autoupdate)
&& YamlElementUtils.equalsListStrings(groups, other.groups)
&& YamlElementUtils.equalsSetStrings(tags, other.tags) && Objects.equals(channel, other.channel)
&& equalsChannels(channels, other.channels) && equalsMetadata(metadata, other.metadata);
}
private boolean equalsChannels(@Nullable Map<@NonNull String, @NonNull Map<@NonNull String, @NonNull Object>> first,
@Nullable Map<@NonNull String, @NonNull Map<@NonNull String, @NonNull Object>> second) {
if (first != null && second != null) {
if (first.size() != second.size()) {
return false;
} else {
return first.entrySet().stream()
.allMatch(e -> YamlElementUtils.equalsConfig(e.getValue(), second.get(e.getKey())));
}
} else {
return first == null && second == null;
}
}
private boolean equalsMetadata(@Nullable Map<@NonNull String, @NonNull YamlMetadataDTO> first,
@Nullable Map<@NonNull String, @NonNull YamlMetadataDTO> second) {
if (first != null && second != null) {
if (first.size() != second.size()) {
return false;
} else {
return first.entrySet().stream().allMatch(e -> e.getValue().equals(second.get(e.getKey())));
}
} else {
return first == null && second == null;
}
}
}

View File

@ -0,0 +1,305 @@
/*
* 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.model.yaml.internal.items;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.AbstractProvider;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemProvider;
import org.openhab.core.items.dto.GroupFunctionDTO;
import org.openhab.core.items.dto.ItemDTOMapper;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.model.yaml.YamlModelListener;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link YamlItemProvider} is an OSGi service, that allows to define items in YAML configuration files.
* Files can be added, updated or removed at runtime.
* These items are automatically exposed to the {@link org.openhab.core.items.ItemRegistry}.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = { ItemProvider.class, YamlItemProvider.class, YamlModelListener.class })
public class YamlItemProvider extends AbstractProvider<Item> implements ItemProvider, YamlModelListener<YamlItemDTO> {
private final Logger logger = LoggerFactory.getLogger(YamlItemProvider.class);
private CoreItemFactory itemFactory;
private final YamlChannelLinkProvider itemChannelLinkProvider;
private final YamlMetadataProvider metaDataProvider;
private final Map<String, Collection<Item>> itemsMap = new ConcurrentHashMap<>();
@Activate
public YamlItemProvider(final @Reference CoreItemFactory itemFactory,
final @Reference YamlChannelLinkProvider itemChannelLinkProvider,
final @Reference YamlMetadataProvider metaDataProvider, Map<String, Object> properties) {
this.itemFactory = itemFactory;
this.itemChannelLinkProvider = itemChannelLinkProvider;
this.metaDataProvider = metaDataProvider;
}
@Deactivate
public void deactivate() {
itemsMap.clear();
}
@Override
public Collection<Item> getAll() {
return itemsMap.values().stream().flatMap(list -> list.stream()).toList();
}
public Collection<Item> getAllFromModel(String modelName) {
return itemsMap.getOrDefault(modelName, List.of());
}
@Override
public Class<YamlItemDTO> getElementClass() {
return YamlItemDTO.class;
}
@Override
public boolean isVersionSupported(int version) {
return version >= 2;
}
@Override
public boolean isDeprecated() {
return false;
}
@Override
public void addedModel(String modelName, Collection<YamlItemDTO> elements) {
Map<Item, YamlItemDTO> added = new LinkedHashMap<>();
elements.forEach(elt -> {
Item item = mapItem(elt);
if (item != null) {
added.put(item, elt);
}
});
Collection<Item> modelItems = Objects
.requireNonNull(itemsMap.computeIfAbsent(modelName, k -> new ArrayList<>()));
modelItems.addAll(added.keySet());
added.forEach((item, itemDTO) -> {
String name = item.getName();
logger.debug("model {} added item {}", modelName, name);
notifyListenersAboutAddedElement(item);
processChannelLinks(modelName, name, itemDTO);
processMetadata(modelName, name, itemDTO);
});
}
@Override
public void updatedModel(String modelName, Collection<YamlItemDTO> elements) {
Map<Item, YamlItemDTO> updated = new LinkedHashMap<>();
elements.forEach(elt -> {
Item item = mapItem(elt);
if (item != null) {
updated.put(item, elt);
}
});
Collection<Item> modelItems = Objects
.requireNonNull(itemsMap.computeIfAbsent(modelName, k -> new ArrayList<>()));
updated.forEach((item, itemDTO) -> {
String name = item.getName();
modelItems.stream().filter(i -> i.getName().equals(name)).findFirst().ifPresentOrElse(oldItem -> {
modelItems.remove(oldItem);
modelItems.add(item);
logger.debug("model {} updated item {}", modelName, name);
notifyListenersAboutUpdatedElement(oldItem, item);
}, () -> {
modelItems.add(item);
logger.debug("model {} added item {}", modelName, name);
notifyListenersAboutAddedElement(item);
});
processChannelLinks(modelName, name, itemDTO);
processMetadata(modelName, name, itemDTO);
});
}
@Override
public void removedModel(String modelName, Collection<YamlItemDTO> elements) {
List<Item> removed = elements.stream().map(elt -> mapItem(elt)).filter(Objects::nonNull).toList();
Collection<Item> modelItems = itemsMap.getOrDefault(modelName, List.of());
removed.forEach(item -> {
String name = item.getName();
modelItems.stream().filter(i -> i.getName().equals(name)).findFirst().ifPresentOrElse(oldItem -> {
modelItems.remove(oldItem);
logger.debug("model {} removed item {}", modelName, name);
notifyListenersAboutRemovedElement(oldItem);
}, () -> logger.debug("model {} item {} not found", modelName, name));
processChannelLinks(modelName, name, null);
processMetadata(modelName, name, null);
});
if (modelItems.isEmpty()) {
itemsMap.remove(modelName);
}
}
private @Nullable Item mapItem(YamlItemDTO itemDTO) {
String name = itemDTO.name;
Item item;
try {
if (GroupItem.TYPE.equalsIgnoreCase(itemDTO.type)) {
YamlGroupDTO groupDTO = itemDTO.group;
if (groupDTO != null) {
Item baseItem = createItemOfType(groupDTO.getBaseType(), name);
if (baseItem != null) {
GroupFunctionDTO groupFunctionDto = new GroupFunctionDTO();
groupFunctionDto.name = groupDTO.getFunction();
groupFunctionDto.params = groupDTO.parameters != null
? groupDTO.parameters.toArray(new String[0])
: new String[0];
item = new GroupItem(name, baseItem, ItemDTOMapper.mapFunction(baseItem, groupFunctionDto));
} else {
item = new GroupItem(name);
}
} else {
item = new GroupItem(name);
}
} else {
item = createItemOfType(itemDTO.getType(), name);
}
} catch (IllegalArgumentException e) {
logger.warn("Error creating item '{}', item will be ignored: {}", name, e.getMessage());
item = null;
}
if (item instanceof GenericItem genericItem) {
genericItem.setLabel(itemDTO.label);
genericItem.setCategory(itemDTO.icon);
if (itemDTO.tags != null) {
for (String tag : itemDTO.tags) {
genericItem.addTag(tag);
}
}
if (itemDTO.groups != null) {
for (String groupName : itemDTO.groups) {
genericItem.addGroupName(groupName);
}
}
}
return item;
}
private @Nullable Item createItemOfType(@Nullable String itemType, String itemName) {
if (itemType == null) {
return null;
}
Item item = itemFactory.createItem(itemType, itemName);
if (item != null) {
logger.debug("Created item '{}' of type '{}'", itemName, itemType);
return item;
}
logger.warn("CoreItemFactory cannot create item '{}' of type '{}'", itemName, itemType);
return null;
}
private void processChannelLinks(String modelName, String itemName, @Nullable YamlItemDTO itemDTO) {
Map<String, Configuration> channelLinks = new HashMap<>(2);
if (itemDTO != null) {
if (itemDTO.channel != null) {
channelLinks.put(itemDTO.channel, new Configuration());
}
if (itemDTO.channels != null) {
itemDTO.channels.forEach((channelUID, config) -> {
channelLinks.put(channelUID, new Configuration(config));
});
}
}
try {
itemChannelLinkProvider.updateItemChannelLinks(modelName, itemName, channelLinks);
} catch (Exception e) {
logger.warn("Channel links configuration of item '{}' could not be parsed correctly.", itemName, e);
}
}
private void processMetadata(String modelName, String itemName, @Nullable YamlItemDTO itemDTO) {
Map<String, YamlMetadataDTO> metadata = new HashMap<>();
if (itemDTO != null) {
boolean hasAutoUpdateMetadata = false;
boolean hasUnitMetadata = false;
boolean hasStateDescriptionMetadata = false;
if (itemDTO.metadata != null) {
for (Map.Entry<String, YamlMetadataDTO> entry : itemDTO.metadata.entrySet()) {
if ("autoupdate".equals(entry.getKey())) {
hasAutoUpdateMetadata = true;
} else if ("unit".equals(entry.getKey())) {
hasUnitMetadata = true;
} else if ("stateDescription".equals(entry.getKey())) {
hasStateDescriptionMetadata = true;
}
Map<String, Object> config = entry.getValue().config;
if (itemDTO.format != null && "stateDescription".equals(entry.getKey())
&& (entry.getValue().config == null || entry.getValue().config.get("pattern") == null)) {
config = new HashMap<>();
if (entry.getValue().config != null) {
for (Map.Entry<String, Object> confEntry : entry.getValue().config.entrySet()) {
config.put(confEntry.getKey(), confEntry.getValue());
}
config.put("pattern", itemDTO.format);
}
}
YamlMetadataDTO mdDTO = new YamlMetadataDTO();
mdDTO.value = entry.getValue().value;
mdDTO.config = config;
metadata.put(entry.getKey(), mdDTO);
}
}
if (!hasAutoUpdateMetadata && itemDTO.autoupdate != null) {
YamlMetadataDTO mdDTO = new YamlMetadataDTO();
mdDTO.value = String.valueOf(itemDTO.autoupdate);
metadata.put("autoupdate", mdDTO);
}
if (!hasUnitMetadata && itemDTO.unit != null) {
YamlMetadataDTO mdDTO = new YamlMetadataDTO();
mdDTO.value = itemDTO.unit;
metadata.put("unit", mdDTO);
}
if (!hasStateDescriptionMetadata && itemDTO.format != null) {
YamlMetadataDTO mdDTO = new YamlMetadataDTO();
mdDTO.config = Map.of("pattern", itemDTO.format);
metadata.put("stateDescription", mdDTO);
}
}
metaDataProvider.updateMetadata(modelName, itemName, metadata);
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.model.yaml.internal.items;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
/**
* The {@link YamlMetadataDTO} is a data transfer object used to serialize a metadata for a particular namespace
* in a YAML configuration file.
*
* @author Laurent Garnier - Initial contribution
*/
public class YamlMetadataDTO {
public String value;
public Map<@NonNull String, @NonNull Object> config;
public YamlMetadataDTO() {
}
public @NonNull String getValue() {
return value == null ? "" : value;
}
@Override
public int hashCode() {
return Objects.hash(getValue());
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
} else if (obj == null || getClass() != obj.getClass()) {
return false;
}
YamlMetadataDTO other = (YamlMetadataDTO) obj;
return Objects.equals(getValue(), other.getValue()) && YamlElementUtils.equalsConfig(config, other.config);
}
}

View File

@ -0,0 +1,103 @@
/*
* 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.model.yaml.internal.items;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.common.registry.AbstractProvider;
import org.openhab.core.items.ItemProvider;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataKey;
import org.openhab.core.items.MetadataProvider;
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class serves as a provider for all metadata that is found within YAML files.
* It is filled with content by the {@link YamlItemProvider}, which cannot itself implement the
* {@link MetadataProvider} interface as it already implements {@link ItemProvider}, which would lead to duplicate
* methods.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(service = { MetadataProvider.class, YamlMetadataProvider.class })
public class YamlMetadataProvider extends AbstractProvider<Metadata> implements MetadataProvider {
private final Logger logger = LoggerFactory.getLogger(YamlMetadataProvider.class);
// Map the metadata to each namespace and then to each item name and finally to each model name
private Map<String, Map<String, Map<String, Metadata>>> metadataMap = new ConcurrentHashMap<>();
@Override
public Collection<Metadata> getAll() {
return metadataMap.values().stream().flatMap(m -> m.values().stream()).flatMap(m -> m.values().stream())
.toList();
}
public Collection<Metadata> getAllFromModel(String modelName) {
return metadataMap.getOrDefault(modelName, Map.of()).values().stream().flatMap(m -> m.values().stream())
.toList();
}
public void updateMetadata(String modelName, String itemName, Map<String, YamlMetadataDTO> metadata) {
Map<String, Map<String, Metadata>> itemsMetadataMap = Objects
.requireNonNull(metadataMap.computeIfAbsent(modelName, k -> new ConcurrentHashMap<>()));
Map<String, Metadata> namespacesMetadataMap = Objects
.requireNonNull(itemsMetadataMap.computeIfAbsent(itemName, k -> new ConcurrentHashMap<>()));
Set<String> namespaceToBeRemoved = new HashSet<>(namespacesMetadataMap.keySet());
for (Map.Entry<String, YamlMetadataDTO> entry : metadata.entrySet()) {
String namespace = entry.getKey();
YamlMetadataDTO mdDTO = entry.getValue();
MetadataKey key = new MetadataKey(namespace, itemName);
Metadata md = new Metadata(key, mdDTO.value == null ? "" : mdDTO.value, mdDTO.config);
namespaceToBeRemoved.remove(namespace);
Metadata oldMd = namespacesMetadataMap.get(namespace);
if (oldMd == null) {
namespacesMetadataMap.put(namespace, md);
logger.debug("notify added metadata {}", md.getUID());
notifyListenersAboutAddedElement(md);
} else if (!md.getValue().equals(oldMd.getValue())
|| !YamlElementUtils.equalsConfig(md.getConfiguration(), oldMd.getConfiguration())) {
namespacesMetadataMap.put(namespace, md);
logger.debug("notify updated metadata {}", md.getUID());
notifyListenersAboutUpdatedElement(oldMd, md);
}
}
namespaceToBeRemoved.forEach(namespace -> {
Metadata md = namespacesMetadataMap.remove(namespace);
if (md != null) {
logger.debug("notify removed metadata {}", md.getUID());
notifyListenersAboutRemovedElement(md);
}
});
if (namespacesMetadataMap.isEmpty()) {
itemsMetadataMap.remove(itemName);
}
if (itemsMetadataMap.isEmpty()) {
metadataMap.remove(modelName);
}
}
}

View File

@ -0,0 +1,253 @@
/*
* 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.model.yaml.internal.items.fileconverter;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.items.GroupFunction;
import org.openhab.core.items.GroupFunction.Equality;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemUtil;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.fileconverter.AbstractItemFileGenerator;
import org.openhab.core.items.fileconverter.ItemFileGenerator;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.model.yaml.YamlElement;
import org.openhab.core.model.yaml.YamlModelRepository;
import org.openhab.core.model.yaml.internal.items.YamlGroupDTO;
import org.openhab.core.model.yaml.internal.items.YamlItemDTO;
import org.openhab.core.model.yaml.internal.items.YamlMetadataDTO;
import org.openhab.core.types.State;
import org.openhab.core.types.StateDescription;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* {@link YamlItemFileConverter} is the YAML file converter for {@link Item} object.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = ItemFileGenerator.class)
public class YamlItemFileConverter extends AbstractItemFileGenerator {
private final YamlModelRepository modelRepository;
private final ConfigDescriptionRegistry configDescriptionRegistry;
@Activate
public YamlItemFileConverter(final @Reference YamlModelRepository modelRepository,
final @Reference ConfigDescriptionRegistry configDescRegistry) {
super();
this.modelRepository = modelRepository;
this.configDescriptionRegistry = configDescRegistry;
}
@Override
public String getFileFormatGenerator() {
return "YAML";
}
@Override
public void generateFileFormat(OutputStream out, List<Item> items, Collection<Metadata> metadata,
boolean hideDefaultParameters) {
List<YamlElement> elements = new ArrayList<>();
items.forEach(item -> {
elements.add(buildItemDTO(item, getChannelLinks(metadata, item.getName()),
getMetadata(metadata, item.getName()), hideDefaultParameters));
});
modelRepository.generateSyntaxFromElements(out, elements);
}
private YamlItemDTO buildItemDTO(Item item, List<Metadata> channelLinks, List<Metadata> metadata,
boolean hideDefaultParameters) {
YamlItemDTO dto = new YamlItemDTO();
dto.name = item.getName();
String label = item.getLabel();
boolean patternSet = false;
String defaultPattern = getDefaultStatePattern(item);
if (label != null && !label.isEmpty()) {
dto.label = item.getLabel();
StateDescription stateDescr = item.getStateDescription();
String statePattern = stateDescr == null ? null : stateDescr.getPattern();
String patterToSet = statePattern != null && !statePattern.equals(defaultPattern) ? statePattern : null;
dto.format = patterToSet;
patternSet = patterToSet != null;
}
dto.type = item.getType();
String mainType = ItemUtil.getMainItemType(item.getType());
String dimension = ItemUtil.getItemTypeExtension(item.getType());
if (CoreItemFactory.NUMBER.equals(mainType) && dimension != null) {
dto.type = mainType;
dto.dimension = dimension;
}
if (item instanceof GroupItem groupItem) {
Item baseItem = groupItem.getBaseItem();
if (baseItem != null) {
dto.group = new YamlGroupDTO();
dto.group.type = baseItem.getType();
mainType = ItemUtil.getMainItemType(baseItem.getType());
dimension = ItemUtil.getItemTypeExtension(baseItem.getType());
if (CoreItemFactory.NUMBER.equals(mainType) && dimension != null) {
dto.group.type = mainType;
dto.group.dimension = dimension;
}
GroupFunction function = groupItem.getFunction();
if (function != null && !(function instanceof Equality)) {
dto.group.function = function.getClass().getSimpleName();
List<String> params = new ArrayList<>();
State[] parameters = function.getParameters();
for (int i = 0; i < parameters.length; i++) {
params.add(parameters[i].toString());
}
dto.group.parameters = params.isEmpty() ? null : params;
}
}
}
String category = item.getCategory();
if (category != null && !category.isEmpty()) {
dto.icon = category;
}
if (!item.getGroupNames().isEmpty()) {
dto.groups = new ArrayList<>();
item.getGroupNames().forEach(group -> {
dto.groups.add(group);
});
}
if (!item.getTags().isEmpty()) {
dto.tags = new LinkedHashSet<>();
item.getTags().stream().sorted().collect(Collectors.toList()).forEach(tag -> {
dto.tags.add(tag);
});
}
if (channelLinks.size() == 1 && channelLinks.getFirst().getConfiguration().isEmpty()) {
dto.channel = channelLinks.getFirst().getValue();
} else if (!channelLinks.isEmpty()) {
dto.channels = new LinkedHashMap<>();
channelLinks.forEach(md -> {
Map<String, Object> configuration = new LinkedHashMap<>();
getConfigurationParameters(md, hideDefaultParameters).forEach(param -> {
configuration.put(param.name(), param.value());
});
dto.channels.put(md.getValue(), configuration);
});
}
Map<String, YamlMetadataDTO> metadataDto = new LinkedHashMap<>();
for (Metadata md : metadata) {
String namespace = md.getUID().getNamespace();
if ("autoupdate".equals(namespace)) {
dto.autoupdate = Boolean.valueOf(md.getValue());
} else if ("unit".equals(namespace)) {
dto.unit = md.getValue();
} else {
YamlMetadataDTO mdDto = new YamlMetadataDTO();
mdDto.value = md.getValue().isEmpty() ? null : md.getValue();
Map<String, Object> configuration = new LinkedHashMap<>();
String statePattern = null;
for (ConfigParameter param : getConfigurationParameters(md)) {
configuration.put(param.name(), param.value());
if ("stateDescription".equals(namespace) && "pattern".equals(param.name())) {
statePattern = param.value().toString();
}
}
// Ignore state description in case it contains only a state pattern and state pattern was injected
// in field format or is the default pattern
if (!(statePattern != null && configuration.size() == 1
&& (patternSet || statePattern.equals(defaultPattern)))) {
mdDto.config = configuration.isEmpty() ? null : configuration;
metadataDto.put(namespace, mdDto);
if (patternSet && statePattern != null) {
dto.format = null;
}
}
}
}
dto.metadata = metadataDto.isEmpty() ? null : metadataDto;
return dto;
}
/*
* Get the list of configuration parameters for a channel link.
*
* If a profile is set and a configuration description is found for this profile, the parameters are provided
* in the same order as in this configuration description, and any parameter having the default value is ignored.
* If no profile is set, the parameters are provided sorted by natural order of their names.
*/
private List<ConfigParameter> getConfigurationParameters(Metadata metadata, boolean hideDefaultParameters) {
List<ConfigParameter> parameters = new ArrayList<>();
Set<String> handledNames = new HashSet<>();
Map<String, Object> configParameters = metadata.getConfiguration();
Object profile = configParameters.get("profile");
List<ConfigDescriptionParameter> configDescriptionParameter = List.of();
if (profile instanceof String profileStr) {
parameters.add(new ConfigParameter("profile", profileStr));
handledNames.add("profile");
try {
ConfigDescription configDesc = configDescriptionRegistry
.getConfigDescription(new URI("profile:" + profileStr));
if (configDesc != null) {
configDescriptionParameter = configDesc.getParameters();
}
} catch (URISyntaxException e) {
// Ignored; in practice this will never be thrown
}
}
for (ConfigDescriptionParameter param : configDescriptionParameter) {
String paramName = param.getName();
if (handledNames.contains(paramName)) {
continue;
}
Object value = configParameters.get(paramName);
Object defaultValue = ConfigUtil.getDefaultValueAsCorrectType(param);
if (value != null && (!hideDefaultParameters || !value.equals(defaultValue))) {
parameters.add(new ConfigParameter(paramName, value));
}
handledNames.add(paramName);
}
for (String paramName : configParameters.keySet().stream().sorted().collect(Collectors.toList())) {
if (handledNames.contains(paramName)) {
continue;
}
Object value = configParameters.get(paramName);
if (value != null) {
parameters.add(new ConfigParameter(paramName, value));
}
handledNames.add(paramName);
}
return parameters;
}
}

View File

@ -38,11 +38,8 @@ public class YamlElementUtils {
public static boolean equalsConfig(@Nullable Map<String, Object> first, @Nullable Map<String, Object> second) { public static boolean equalsConfig(@Nullable Map<String, Object> first, @Nullable Map<String, Object> second) {
if (first != null && second != null) { if (first != null && second != null) {
if (first.size() != second.size()) { return first.size() != second.size() ? false
return false; : first.entrySet().stream().allMatch(e -> equalsConfigValue(e.getValue(), second.get(e.getKey())));
} else {
return first.entrySet().stream().allMatch(e -> equalsConfigValue(e.getValue(), second.get(e.getKey())));
}
} else { } else {
return first == null && second == null; return first == null && second == null;
} }
@ -54,6 +51,22 @@ public class YamlElementUtils {
: first.equals(second); : first.equals(second);
} }
public static boolean equalsListStrings(@Nullable List<String> first, @Nullable List<String> second) {
if (first != null && second != null) {
return Arrays.equals(first.toArray(), second.toArray());
} else {
return first == null && second == null;
}
}
public static boolean equalsSetStrings(@Nullable Set<String> first, @Nullable Set<String> second) {
if (first != null && second != null) {
return first.size() != second.size() ? false : first.containsAll(second);
} else {
return first == null && second == null;
}
}
public static @Nullable String getAdjustedItemType(@Nullable String type) { public static @Nullable String getAdjustedItemType(@Nullable String type) {
return type == null ? null : StringUtils.capitalize(type); return type == null ? null : StringUtils.capitalize(type);
} }

View File

@ -0,0 +1,147 @@
/*
* 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.model.yaml.internal.items;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* The {@link YamlGroupDTOTest} contains tests for the {@link YamlGroupDTO} class.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class YamlGroupDTOTest {
@Test
public void testGetBaseType() throws IOException {
YamlGroupDTO gr = new YamlGroupDTO();
assertEquals(null, gr.getBaseType());
gr.type = "Number";
assertEquals("Number", gr.getBaseType());
gr.type = "number";
assertEquals("Number", gr.getBaseType());
gr.dimension = "Dimensionless";
assertEquals("Number:Dimensionless", gr.getBaseType());
gr.dimension = "dimensionless";
assertEquals("Number:Dimensionless", gr.getBaseType());
}
@Test
public void testGetFunction() throws IOException {
YamlGroupDTO gr = new YamlGroupDTO();
assertEquals("EQUALITY", gr.getFunction());
gr.function = "AND";
assertEquals("AND", gr.getFunction());
gr.function = "or";
assertEquals("OR", gr.getFunction());
gr.function = "Min";
assertEquals("MIN", gr.getFunction());
}
@Test
public void testIsValid() throws IOException {
List<String> err = new ArrayList<>();
List<String> warn = new ArrayList<>();
YamlGroupDTO gr = new YamlGroupDTO();
assertTrue(gr.isValid(err, warn));
gr.type = "String";
assertTrue(gr.isValid(err, warn));
gr.type = "string";
assertTrue(gr.isValid(err, warn));
gr.type = "Other";
assertFalse(gr.isValid(err, warn));
gr.type = "Number";
assertTrue(gr.isValid(err, warn));
gr.dimension = "Dimensionless";
assertTrue(gr.isValid(err, warn));
gr.type = "number";
gr.dimension = "dimensionless";
assertTrue(gr.isValid(err, warn));
gr.dimension = "Other";
assertFalse(gr.isValid(err, warn));
gr.type = "Color";
gr.dimension = null;
assertEquals("Color", gr.getBaseType());
gr.function = "AND";
assertTrue(gr.isValid(err, warn));
gr.function = "or";
assertTrue(gr.isValid(err, warn));
gr.function = "Min";
assertTrue(gr.isValid(err, warn));
gr.function = "invalid";
assertFalse(gr.isValid(err, warn));
}
@Test
public void testEquals() throws IOException {
YamlGroupDTO gr1 = new YamlGroupDTO();
YamlGroupDTO gr2 = new YamlGroupDTO();
gr1.type = "String";
gr2.type = "String";
assertTrue(gr1.equals(gr2));
gr1.type = "String";
gr2.type = "Number";
assertFalse(gr1.equals(gr2));
gr1.type = "Number";
gr2.type = "Number";
assertTrue(gr1.equals(gr2));
gr2.type = "number";
assertTrue(gr1.equals(gr2));
gr1.dimension = "Temperature";
assertFalse(gr1.equals(gr2));
gr2.dimension = "Humidity";
assertFalse(gr1.equals(gr2));
gr2.dimension = "Temperature";
assertTrue(gr1.equals(gr2));
gr2.dimension = "temperature";
assertTrue(gr1.equals(gr2));
gr1.function = "or";
gr2.function = null;
assertFalse(gr1.equals(gr2));
gr1.function = null;
gr2.function = "or";
assertFalse(gr1.equals(gr2));
gr1.function = "or";
gr2.function = "or";
assertTrue(gr1.equals(gr2));
gr2.function = "Or";
assertTrue(gr1.equals(gr2));
gr2.function = "OR";
assertTrue(gr1.equals(gr2));
gr1.parameters = List.of("ON", "OFF");
gr2.parameters = null;
assertFalse(gr1.equals(gr2));
gr2.parameters = List.of("ON");
assertFalse(gr1.equals(gr2));
gr1.parameters = null;
gr2.parameters = List.of("ON", "OFF");
assertFalse(gr1.equals(gr2));
gr1.parameters = List.of("ON");
assertFalse(gr1.equals(gr2));
gr1.parameters = List.of("ON", "OFF");
assertTrue(gr1.equals(gr2));
}
}

View File

@ -0,0 +1,400 @@
/*
* 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.model.yaml.internal.items;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* The {@link YamlItemDTOTest} contains tests for the {@link YamlItemDTO} class.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class YamlItemDTOTest {
@Test
public void testGetType() throws IOException {
YamlItemDTO item = new YamlItemDTO();
assertEquals(null, item.getType());
item.type = "Number";
assertEquals("Number", item.getType());
item.type = "number";
assertEquals("Number", item.getType());
item.dimension = "Dimensionless";
assertEquals("Number:Dimensionless", item.getType());
item.dimension = "dimensionless";
assertEquals("Number:Dimensionless", item.getType());
}
@Test
public void testIsValid() throws IOException {
YamlItemDTO item = new YamlItemDTO();
assertFalse(item.isValid(null, null));
item.type = "Switch";
item.name = "name";
assertTrue(item.isValid(null, null));
item.name = "$name";
assertFalse(item.isValid(null, null));
item.name = "my-name";
assertTrue(item.isValid(null, null));
item.type = "Group";
assertTrue(item.isValid(null, null));
item.type = "GRoup";
assertTrue(item.isValid(null, null));
item.group = new YamlGroupDTO();
item.group.type = "Switch";
item.group.function = "OR";
item.group.parameters = List.of("ON", "OFF");
assertTrue(item.isValid(null, null));
item.type = "String";
assertTrue(item.isValid(null, null));
item.group = null;
item.type = "string";
assertTrue(item.isValid(null, null));
item.type = "Other";
assertFalse(item.isValid(null, null));
item.type = "Number";
assertTrue(item.isValid(null, null));
item.dimension = "Dimensionless";
assertTrue(item.isValid(null, null));
item.type = "number";
item.dimension = "dimensionless";
assertTrue(item.isValid(null, null));
item.dimension = "Other";
assertFalse(item.isValid(null, null));
item.type = "Color";
item.dimension = null;
assertTrue(item.isValid(null, null));
item.label = "My label";
assertTrue(item.isValid(null, null));
item.icon = "xx:source:set:icon";
assertFalse(item.isValid(null, null));
item.icon = "source:set:icon";
assertTrue(item.isValid(null, null));
item.icon = "icon-source:$icon-set:my_icon";
assertFalse(item.isValid(null, null));
item.icon = "icon-source:icon-set:my_icon";
assertTrue(item.isValid(null, null));
item.groups = List.of("group1", "group 2");
assertFalse(item.isValid(null, null));
item.groups = List.of("group1", "group2");
assertTrue(item.isValid(null, null));
item.tags = Set.of("Tag1", "Tag 2");
assertTrue(item.isValid(null, null));
item.channel = "binding:type:uid:channelid";
assertTrue(item.isValid(null, null));
item.channel = "binding:type:uid:group#channelid";
assertTrue(item.isValid(null, null));
item.channel = "binding:type:channelid";
assertFalse(item.isValid(null, null));
item.channel = "binding:$type:uid:group#channelid";
assertFalse(item.isValid(null, null));
item.channel = "binding:type:uid:group$channelid";
assertFalse(item.isValid(null, null));
}
@Test
public void testEquals() throws IOException {
YamlItemDTO item1 = new YamlItemDTO();
YamlItemDTO item2 = new YamlItemDTO();
item1.name = "item-name";
item2.name = "item-name-2";
assertFalse(item1.equals(item2));
item2.name = "item-name";
assertTrue(item1.equals(item2));
item1.type = "Number";
item1.dimension = "Temperature";
item2.type = "Number";
item2.dimension = "Temperature";
assertTrue(item1.equals(item2));
item1.label = "A label";
item2.label = "A label";
assertTrue(item1.equals(item2));
item1.icon = "oh:classic:temperature";
item2.icon = "oh:classic:temperature";
assertTrue(item1.equals(item2));
item1.groups = List.of("group1", "group2");
item2.groups = List.of("group1", "group2");
assertTrue(item1.equals(item2));
item1.tags = Set.of("Tag1", "Tag 2");
item2.tags = Set.of("Tag1", "Tag 2");
assertTrue(item1.equals(item2));
item2.tags = Set.of("Tag 2", "Tag1");
assertTrue(item1.equals(item2));
item1.group = new YamlGroupDTO();
item1.group.type = "Switch";
item1.group.function = "OR";
item1.group.parameters = List.of("ON", "OFF");
item2.group = new YamlGroupDTO();
item2.group.type = "Switch";
item2.group.function = "OR";
item2.group.parameters = List.of("ON", "OFF");
assertTrue(item1.equals(item2));
}
@Test
public void testEqualsWithLabel() throws IOException {
YamlItemDTO item1 = new YamlItemDTO();
YamlItemDTO item2 = new YamlItemDTO();
item1.name = "item-name";
item2.name = "item-name";
item1.type = "String";
item2.type = "String";
item1.label = null;
item2.label = null;
assertTrue(item1.equals(item2));
item1.label = "A label";
item2.label = null;
assertFalse(item1.equals(item2));
item1.label = null;
item2.label = "A label";
assertFalse(item1.equals(item2));
item1.label = "A label";
item2.label = "A different label";
assertFalse(item1.equals(item2));
item1.label = "A label";
item2.label = "A label";
assertTrue(item1.equals(item2));
}
@Test
public void testEqualsWithIcon() throws IOException {
YamlItemDTO item1 = new YamlItemDTO();
YamlItemDTO item2 = new YamlItemDTO();
item1.name = "item-name";
item2.name = "item-name";
item1.type = "Number";
item2.type = "Number";
item1.icon = null;
item2.icon = null;
assertTrue(item1.equals(item2));
item1.icon = "humidity";
item2.icon = null;
assertFalse(item1.equals(item2));
item1.icon = null;
item2.icon = "humidity";
assertFalse(item1.equals(item2));
item1.icon = "humidity";
item2.icon = "temperature";
assertFalse(item1.equals(item2));
item1.icon = "humidity";
item2.icon = "humidity";
assertTrue(item1.equals(item2));
}
@Test
public void testEqualsWithFormat() throws IOException {
YamlItemDTO item1 = new YamlItemDTO();
YamlItemDTO item2 = new YamlItemDTO();
item1.name = "item-name";
item2.name = "item-name";
item1.type = "Number";
item2.type = "Number";
item1.format = null;
item2.format = null;
assertTrue(item1.equals(item2));
item1.format = "%.1f °C";
item2.format = null;
assertFalse(item1.equals(item2));
item1.format = null;
item2.format = "%.1f °C";
assertFalse(item1.equals(item2));
item1.format = "%.1f °C";
item2.format = "%.0f °C";
assertFalse(item1.equals(item2));
item1.format = "%.1f °C";
item2.format = "%.1f °C";
assertTrue(item1.equals(item2));
}
@Test
public void testEqualsWithUnit() throws IOException {
YamlItemDTO item1 = new YamlItemDTO();
YamlItemDTO item2 = new YamlItemDTO();
item1.name = "item-name";
item2.name = "item-name";
item1.type = "Number";
item2.type = "Number";
item1.unit = null;
item2.unit = null;
assertTrue(item1.equals(item2));
item1.unit = "°C";
item2.unit = null;
assertFalse(item1.equals(item2));
item1.unit = null;
item2.unit = "°C";
assertFalse(item1.equals(item2));
item1.unit = "°C";
item2.unit = "°F";
assertFalse(item1.equals(item2));
item1.unit = "°C";
item2.unit = "°C";
assertTrue(item1.equals(item2));
}
@Test
public void testEqualsWithAutoupdate() throws IOException {
YamlItemDTO item1 = new YamlItemDTO();
YamlItemDTO item2 = new YamlItemDTO();
item1.name = "item-name";
item2.name = "item-name";
item1.type = "Number";
item2.type = "Number";
item1.autoupdate = null;
item2.autoupdate = null;
assertTrue(item1.equals(item2));
item1.autoupdate = false;
item2.autoupdate = true;
assertFalse(item1.equals(item2));
item1.autoupdate = true;
item2.autoupdate = false;
assertFalse(item1.equals(item2));
item1.autoupdate = true;
item2.autoupdate = null;
assertFalse(item1.equals(item2));
item1.autoupdate = null;
item2.autoupdate = true;
assertFalse(item1.equals(item2));
item1.autoupdate = false;
item2.autoupdate = null;
assertFalse(item1.equals(item2));
item1.autoupdate = null;
item2.autoupdate = false;
assertFalse(item1.equals(item2));
item1.autoupdate = false;
item2.autoupdate = false;
assertTrue(item1.equals(item2));
item1.autoupdate = true;
item2.autoupdate = true;
assertTrue(item1.equals(item2));
}
// @Test
// public void testEqualsWithConfigurations() throws IOException {
// YamlThingDTO th1 = new YamlThingDTO();
// YamlThingDTO th2 = new YamlThingDTO();
//
// th1.uid = "binding:type:id";
// th2.uid = "binding:type:id";
//
// th1.config = null;
// th2.config = null;
// assertTrue(th1.equals(th2));
// th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// th2.config = null;
// assertFalse(th1.equals(th2));
// th1.config = null;
// th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// assertFalse(th1.equals(th2));
// th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// th2.config = Map.of("param1", "other value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// assertFalse(th1.equals(th2));
// th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// th2.config = Map.of("param1", "value", "param2", 25, "param3", true, "param4", List.of("val 1", "val 2"));
// assertFalse(th1.equals(th2));
// th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// th2.config = Map.of("param1", "value", "param2", 50, "param3", false, "param4", List.of("val 1", "val 2"));
// assertFalse(th1.equals(th2));
// th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "value 2"));
// assertFalse(th1.equals(th2));
// th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1"));
// assertFalse(th1.equals(th2));
// th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", 75);
// assertFalse(th1.equals(th2));
// th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// th2.config = Map.of("param1", "value", "param2", 50, "param3", true);
// assertFalse(th1.equals(th2));
// th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
// assertTrue(th1.equals(th2));
// }
//
// @Test
// public void testEqualsWithChannels() throws IOException {
// YamlThingDTO th1 = new YamlThingDTO();
// th1.uid = "binding:type:id";
// YamlThingDTO th2 = new YamlThingDTO();
// th2.uid = "binding:type:id";
//
// YamlChannelDTO ch1 = new YamlChannelDTO();
// ch1.type = "channel-type";
// YamlChannelDTO ch2 = new YamlChannelDTO();
// ch2.type = "channel-other-type";
// YamlChannelDTO ch3 = new YamlChannelDTO();
// ch3.type = "channel-type";
// YamlChannelDTO ch4 = new YamlChannelDTO();
// ch4.type = "channel-other-type";
//
// th1.channels = Map.of("channel1", ch1);
// th2.channels = Map.of("channel1", ch3);
// assertTrue(th1.equals(th2));
//
// th1.channels = Map.of("channel1", ch1, "channel2", ch2);
// th2.channels = Map.of("channel1", ch3, "channel2", ch4);
// assertTrue(th1.equals(th2));
//
// th1.channels = Map.of("channel1", ch1);
// th2.channels = null;
// assertFalse(th1.equals(th2));
//
// th1.channels = null;
// th2.channels = Map.of("channel1", ch3);
// assertFalse(th1.equals(th2));
//
// th1.channels = Map.of("channel1", ch1, "channel2", ch2);
// th2.channels = Map.of("channel1", ch3);
// assertFalse(th1.equals(th2));
//
// th1.channels = Map.of("channel1", ch1);
// th2.channels = Map.of("channel1", ch4);
// assertFalse(th1.equals(th2));
//
// th1.channels = Map.of("channel", ch1);
// th2.channels = Map.of("channel1", ch3);
// assertFalse(th1.equals(th2));
// }
}

View File

@ -41,7 +41,7 @@ import org.osgi.service.component.annotations.Reference;
* @author Kai Kreuzer - Initial contribution * @author Kai Kreuzer - Initial contribution
* @author Alexander Kostadinov - Initial contribution * @author Alexander Kostadinov - Initial contribution
*/ */
@Component(immediate = true) @Component(immediate = true, service = { CoreItemFactory.class, ItemFactory.class })
@NonNullByDefault @NonNullByDefault
public class CoreItemFactory implements ItemFactory { public class CoreItemFactory implements ItemFactory {