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
parent
53ddb0c29c
commit
bc62a202ee
|
@ -160,7 +160,7 @@ public class FileFormatResource implements RESTResource {
|
|||
MyItem:
|
||||
type: Switch
|
||||
label: Label
|
||||
category: icon
|
||||
icon: icon
|
||||
groups:
|
||||
- Group1
|
||||
- Group2
|
||||
|
|
|
@ -40,6 +40,7 @@ import org.openhab.core.model.yaml.YamlElement;
|
|||
import org.openhab.core.model.yaml.YamlElementName;
|
||||
import org.openhab.core.model.yaml.YamlModelListener;
|
||||
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.things.YamlThingDTO;
|
||||
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 Set<String> KNOWN_ELEMENTS = Set.of( //
|
||||
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] ";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -38,11 +38,8 @@ public class YamlElementUtils {
|
|||
|
||||
public static boolean equalsConfig(@Nullable Map<String, Object> first, @Nullable Map<String, Object> second) {
|
||||
if (first != null && second != null) {
|
||||
if (first.size() != second.size()) {
|
||||
return false;
|
||||
} else {
|
||||
return first.entrySet().stream().allMatch(e -> equalsConfigValue(e.getValue(), second.get(e.getKey())));
|
||||
}
|
||||
return first.size() != second.size() ? false
|
||||
: first.entrySet().stream().allMatch(e -> equalsConfigValue(e.getValue(), second.get(e.getKey())));
|
||||
} else {
|
||||
return first == null && second == null;
|
||||
}
|
||||
|
@ -54,6 +51,22 @@ public class YamlElementUtils {
|
|||
: 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) {
|
||||
return type == null ? null : StringUtils.capitalize(type);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
// }
|
||||
}
|
|
@ -41,7 +41,7 @@ import org.osgi.service.component.annotations.Reference;
|
|||
* @author Kai Kreuzer - Initial contribution
|
||||
* @author Alexander Kostadinov - Initial contribution
|
||||
*/
|
||||
@Component(immediate = true)
|
||||
@Component(immediate = true, service = { CoreItemFactory.class, ItemFactory.class })
|
||||
@NonNullByDefault
|
||||
public class CoreItemFactory implements ItemFactory {
|
||||
|
||||
|
|
Loading…
Reference in New Issue