Skip to content

Commit bc62a20

Browse files
authored
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>
1 parent 53ddb0c commit bc62a20

File tree

13 files changed

+1800
-8
lines changed

13 files changed

+1800
-8
lines changed

bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public class FileFormatResource implements RESTResource {
160160
MyItem:
161161
type: Switch
162162
label: Label
163-
category: icon
163+
icon: icon
164164
groups:
165165
- Group1
166166
- Group2

bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepositoryImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.openhab.core.model.yaml.YamlElementName;
4141
import org.openhab.core.model.yaml.YamlModelListener;
4242
import org.openhab.core.model.yaml.YamlModelRepository;
43+
import org.openhab.core.model.yaml.internal.items.YamlItemDTO;
4344
import org.openhab.core.model.yaml.internal.semantics.YamlSemanticTagDTO;
4445
import org.openhab.core.model.yaml.internal.things.YamlThingDTO;
4546
import org.openhab.core.service.WatchService;
@@ -86,7 +87,8 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
8687
private static final String READ_ONLY = "readOnly";
8788
private static final Set<String> KNOWN_ELEMENTS = Set.of( //
8889
getElementName(YamlSemanticTagDTO.class), // "tags"
89-
getElementName(YamlThingDTO.class) // "things"
90+
getElementName(YamlThingDTO.class), // "things"
91+
getElementName(YamlItemDTO.class) // "items"
9092
);
9193

9294
private static final String UNWANTED_EXCEPTION_TEXT = "at [Source: UNKNOWN; byte offset: #UNKNOWN] ";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright (c) 2010-2025 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.core.model.yaml.internal.items;
14+
15+
import java.util.Collection;
16+
import java.util.HashSet;
17+
import java.util.Map;
18+
import java.util.Objects;
19+
import java.util.Set;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
22+
import org.eclipse.jdt.annotation.NonNullByDefault;
23+
import org.openhab.core.common.registry.AbstractProvider;
24+
import org.openhab.core.config.core.Configuration;
25+
import org.openhab.core.items.ItemProvider;
26+
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
27+
import org.openhab.core.thing.ChannelUID;
28+
import org.openhab.core.thing.link.ItemChannelLink;
29+
import org.openhab.core.thing.link.ItemChannelLinkProvider;
30+
import org.openhab.core.thing.profiles.ProfileTypeUID;
31+
import org.osgi.service.component.annotations.Component;
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
34+
35+
/**
36+
* This class serves as a provider for all item channel links that is found within YAML files.
37+
* It is filled with content by the {@link YamlItemProvider}, which cannot itself implement the
38+
* {@link ItemChannelLinkProvider} interface as it already implements {@link ItemProvider},
39+
* which would lead to duplicate methods.
40+
*
41+
* @author Laurent Garnier - Initial contribution
42+
*/
43+
@NonNullByDefault
44+
@Component(immediate = true, service = { ItemChannelLinkProvider.class, YamlChannelLinkProvider.class })
45+
public class YamlChannelLinkProvider extends AbstractProvider<ItemChannelLink> implements ItemChannelLinkProvider {
46+
47+
private final Logger logger = LoggerFactory.getLogger(YamlChannelLinkProvider.class);
48+
49+
// Map the channel links to each channel UID and then to each item name and finally to each model name
50+
private Map<String, Map<String, Map<ChannelUID, ItemChannelLink>>> itemsChannelLinksMap = new ConcurrentHashMap<>();
51+
52+
@Override
53+
public Collection<ItemChannelLink> getAll() {
54+
return itemsChannelLinksMap.values().stream().flatMap(m -> m.values().stream())
55+
.flatMap(m -> m.values().stream()).toList();
56+
}
57+
58+
public Collection<ItemChannelLink> getAllFromModel(String modelName) {
59+
return itemsChannelLinksMap.getOrDefault(modelName, Map.of()).values().stream()
60+
.flatMap(m -> m.values().stream()).toList();
61+
}
62+
63+
public void updateItemChannelLinks(String modelName, String itemName, Map<String, Configuration> channelLinks) {
64+
Map<String, Map<ChannelUID, ItemChannelLink>> channelLinksMap = Objects
65+
.requireNonNull(itemsChannelLinksMap.computeIfAbsent(modelName, k -> new ConcurrentHashMap<>()));
66+
// Create a HashMap with an initial capacity of 2 (the default is 16) to save memory because most items have
67+
// only one channel. A capacity of 2 is enough to avoid resizing the HashMap in most cases, whereas 1 would
68+
// trigger a resize as soon as one element is added.
69+
Map<ChannelUID, ItemChannelLink> links = Objects
70+
.requireNonNull(channelLinksMap.computeIfAbsent(itemName, k -> new ConcurrentHashMap<>(2)));
71+
72+
Set<ChannelUID> linksToBeRemoved = new HashSet<>(links.keySet());
73+
74+
for (Map.Entry<String, Configuration> entry : channelLinks.entrySet()) {
75+
String channelUID = entry.getKey();
76+
Configuration configuration = entry.getValue();
77+
78+
ChannelUID channelUIDObject;
79+
try {
80+
channelUIDObject = new ChannelUID(channelUID);
81+
} catch (IllegalArgumentException e) {
82+
logger.warn("Invalid channel UID '{}' in channel link for item '{}'!", channelUID, itemName, e);
83+
continue;
84+
}
85+
86+
// Fix the configuration in case a profile is defined without any scope
87+
if (configuration.containsKey("profile") && configuration.get("profile") instanceof String profile
88+
&& profile.indexOf(":") == -1) {
89+
String fullProfile = ProfileTypeUID.SYSTEM_SCOPE + ":" + profile;
90+
configuration.put("profile", fullProfile);
91+
logger.info(
92+
"Profile '{}' for channel '{}' is missing the scope prefix, assuming the correct UID is '{}'. Check your configuration.",
93+
profile, channelUID, fullProfile);
94+
}
95+
96+
ItemChannelLink itemChannelLink = new ItemChannelLink(itemName, channelUIDObject, configuration);
97+
98+
linksToBeRemoved.remove(channelUIDObject);
99+
ItemChannelLink oldLink = links.get(channelUIDObject);
100+
if (oldLink == null) {
101+
links.put(channelUIDObject, itemChannelLink);
102+
logger.debug("notify added item channel link {}", itemChannelLink.getUID());
103+
notifyListenersAboutAddedElement(itemChannelLink);
104+
} else if (!YamlElementUtils.equalsConfig(configuration.getProperties(),
105+
oldLink.getConfiguration().getProperties())) {
106+
links.put(channelUIDObject, itemChannelLink);
107+
logger.debug("notify updated item channel link {}", itemChannelLink.getUID());
108+
notifyListenersAboutUpdatedElement(oldLink, itemChannelLink);
109+
}
110+
}
111+
112+
linksToBeRemoved.forEach(uid -> {
113+
ItemChannelLink link = links.remove(uid);
114+
if (link != null) {
115+
logger.debug("notify removed item channel link {}", link.getUID());
116+
notifyListenersAboutRemovedElement(link);
117+
}
118+
});
119+
if (links.isEmpty()) {
120+
channelLinksMap.remove(itemName);
121+
}
122+
if (channelLinksMap.isEmpty()) {
123+
itemsChannelLinksMap.remove(modelName);
124+
}
125+
}
126+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (c) 2010-2025 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.core.model.yaml.internal.items;
14+
15+
import java.util.List;
16+
import java.util.Objects;
17+
import java.util.Set;
18+
19+
import org.eclipse.jdt.annotation.NonNull;
20+
import org.eclipse.jdt.annotation.Nullable;
21+
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
22+
23+
/**
24+
* The {@link YamlGroupDTO} is a data transfer object used to serialize the details of a group item
25+
* in a YAML configuration file.
26+
*
27+
* @author Laurent Garnier - Initial contribution
28+
*/
29+
public class YamlGroupDTO {
30+
31+
private static final String DEFAULT_FUNCTION = "EQUALITY";
32+
private static final Set<String> VALID_FUNCTIONS = Set.of("AND", "OR", "NAND", "NOR", "XOR", "COUNT", "AVG",
33+
"MEDIAN", "SUM", "MIN", "MAX", "LATEST", "EARLIEST", DEFAULT_FUNCTION);
34+
35+
public String type;
36+
public String dimension;
37+
public String function;
38+
public List<@NonNull String> parameters;
39+
40+
public YamlGroupDTO() {
41+
}
42+
43+
public boolean isValid(@NonNull List<@NonNull String> errors, @NonNull List<@NonNull String> warnings) {
44+
boolean ok = true;
45+
if (!YamlElementUtils.isValidItemType(type)) {
46+
errors.add("invalid value \"%s\" for \"type\" field in group".formatted(type));
47+
ok = false;
48+
} else if (YamlElementUtils.isNumberItemType(type)) {
49+
if (!YamlElementUtils.isValidItemDimension(dimension)) {
50+
errors.add("invalid value \"%s\" for \"dimension\" field in group".formatted(dimension));
51+
ok = false;
52+
}
53+
} else if (dimension != null) {
54+
warnings.add("\"dimension\" field in group ignored as type is not Number");
55+
}
56+
if (!VALID_FUNCTIONS.contains(getFunction())) {
57+
errors.add("invalid value \"%s\" for \"function\" field".formatted(function));
58+
ok = false;
59+
}
60+
return ok;
61+
}
62+
63+
public @Nullable String getBaseType() {
64+
return YamlElementUtils.getItemTypeWithDimension(type, dimension);
65+
}
66+
67+
public String getFunction() {
68+
return function != null ? function.toUpperCase() : DEFAULT_FUNCTION;
69+
}
70+
71+
@Override
72+
public int hashCode() {
73+
return Objects.hash(getBaseType(), getFunction());
74+
}
75+
76+
@Override
77+
public boolean equals(@Nullable Object obj) {
78+
if (this == obj) {
79+
return true;
80+
} else if (obj == null || getClass() != obj.getClass()) {
81+
return false;
82+
}
83+
YamlGroupDTO other = (YamlGroupDTO) obj;
84+
return Objects.equals(getBaseType(), other.getBaseType()) && Objects.equals(getFunction(), other.getFunction())
85+
&& YamlElementUtils.equalsListStrings(parameters, other.parameters);
86+
}
87+
}

0 commit comments

Comments
 (0)