Skip to content

Commit 57c1e31

Browse files
committed
Provide a hint when an Item's semantic structure is incorrect
Signed-off-by: Jimmy Tanagra <[email protected]>
1 parent fd53c4c commit 57c1e31

File tree

3 files changed

+527
-2
lines changed

3 files changed

+527
-2
lines changed

bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java

+38
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,44 @@ public class SemanticTags {
8181
}
8282
}
8383

84+
/**
85+
* Determines the semantic root of a given tag type.
86+
*
87+
* @param type the tag type
88+
* @return the semantic root of the tag type, or null if the type is not a semantic tag
89+
*/
90+
public static @Nullable Class<? extends Tag> getSemanticRoot(Class<? extends Tag> type) {
91+
if (type == null) {
92+
return null;
93+
}
94+
if (Point.class.isAssignableFrom(type)) {
95+
return Point.class;
96+
} else if (Property.class.isAssignableFrom(type)) {
97+
return Property.class;
98+
} else if (Location.class.isAssignableFrom(type)) {
99+
return Location.class;
100+
} else if (Equipment.class.isAssignableFrom(type)) {
101+
return Equipment.class;
102+
} else {
103+
return null;
104+
}
105+
}
106+
107+
/**
108+
* Determines the name of the semantic root of a given tag type.
109+
*
110+
* @param type the tag type
111+
* @return the name of the semantic root of the tag type, or an empty string if the type is not a semantic tag
112+
*/
113+
public static String getSemanticRootName(Class<? extends Tag> type) {
114+
Class<? extends Tag> semanticRoot = getSemanticRoot(type);
115+
if (semanticRoot != null) {
116+
return semanticRoot.getSimpleName();
117+
} else {
118+
return "";
119+
}
120+
}
121+
84122
/**
85123
* Determines the {@link Property} type that a {@link Point} relates to.
86124
*

bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java

+212-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.HashSet;
1818
import java.util.List;
1919
import java.util.Locale;
20+
import java.util.Objects;
2021
import java.util.Optional;
2122
import java.util.Set;
2223
import java.util.function.Predicate;
@@ -25,8 +26,10 @@
2526

2627
import org.eclipse.jdt.annotation.NonNullByDefault;
2728
import org.eclipse.jdt.annotation.Nullable;
29+
import org.openhab.core.common.registry.RegistryChangeListener;
2830
import org.openhab.core.items.GroupItem;
2931
import org.openhab.core.items.Item;
32+
import org.openhab.core.items.ItemNotFoundException;
3033
import org.openhab.core.items.ItemPredicates;
3134
import org.openhab.core.items.ItemRegistry;
3235
import org.openhab.core.items.Metadata;
@@ -35,27 +38,33 @@
3538
import org.openhab.core.semantics.Equipment;
3639
import org.openhab.core.semantics.Location;
3740
import org.openhab.core.semantics.Point;
41+
import org.openhab.core.semantics.Property;
3842
import org.openhab.core.semantics.SemanticTag;
3943
import org.openhab.core.semantics.SemanticTagRegistry;
44+
import org.openhab.core.semantics.SemanticTags;
4045
import org.openhab.core.semantics.SemanticsPredicates;
4146
import org.openhab.core.semantics.SemanticsService;
4247
import org.openhab.core.semantics.Tag;
4348
import org.osgi.service.component.annotations.Activate;
4449
import org.osgi.service.component.annotations.Component;
50+
import org.osgi.service.component.annotations.Deactivate;
4551
import org.osgi.service.component.annotations.Reference;
4652

4753
/**
4854
* The internal implementation of the {@link SemanticsService} interface, which is registered as an OSGi service.
4955
*
5056
* @author Kai Kreuzer - Initial contribution
5157
* @author Laurent Garnier - Few methods moved from class SemanticTags in order to use the semantic tag registry
58+
* @author Jimmy Tanagra - Add Item semantic tag validation
5259
*/
5360
@NonNullByDefault
54-
@Component
55-
public class SemanticsServiceImpl implements SemanticsService {
61+
@Component(immediate = true)
62+
public class SemanticsServiceImpl implements SemanticsService, RegistryChangeListener<Item> {
5663

5764
private static final String SYNONYMS_NAMESPACE = "synonyms";
5865

66+
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SemanticsServiceImpl.class);
67+
5968
private final ItemRegistry itemRegistry;
6069
private final MetadataRegistry metadataRegistry;
6170
private final SemanticTagRegistry semanticTagRegistry;
@@ -67,6 +76,14 @@ public SemanticsServiceImpl(final @Reference ItemRegistry itemRegistry,
6776
this.itemRegistry = itemRegistry;
6877
this.metadataRegistry = metadataRegistry;
6978
this.semanticTagRegistry = semanticTagRegistry;
79+
80+
this.itemRegistry.stream().forEach(this::checkSemantics);
81+
this.itemRegistry.addRegistryChangeListener(this);
82+
}
83+
84+
@Deactivate
85+
public void deactivate() {
86+
itemRegistry.removeRegistryChangeListener(this);
7087
}
7188

7289
@Override
@@ -158,4 +175,197 @@ private List<String> getLabelAndSynonyms(SemanticTag tag, Locale locale) {
158175
Stream<String> synonyms = localizedTag.getSynonyms().stream();
159176
return Stream.concat(label, synonyms).map(s -> s.toLowerCase(locale)).distinct().toList();
160177
}
178+
179+
/**
180+
* Validates the semantic tags of an item.
181+
*
182+
* It returns true only if one of the following is true:
183+
* - No semantic tags at all
184+
* - Only one Semantic tag of any kind.
185+
* - Note: having only one Property tag is allowed. It implies that the item is a Point.
186+
* - One Point tag and one Property tag
187+
*
188+
* It returns false if two Semantic tags are found, but they don't consist of one Point and one Property.
189+
* It would also return false if more than two Semantic tags are found.
190+
*
191+
* @param item
192+
* @param semanticTag the determined semantic tag of the item
193+
* @return true if the item contains no semantic tags, or a valid combination of semantic tags, otherwise false
194+
*/
195+
boolean validateTags(Item item, @Nullable Class<? extends Tag> semanticTag) {
196+
if (semanticTag == null) {
197+
return true;
198+
}
199+
String semanticType = SemanticTags.getSemanticRootName(semanticTag);
200+
// We're using Collectors here instead of Stream.toList() to resolve Java's wildcard capture conversion issue
201+
List<Class<? extends Tag>> tags = item.getTags().stream().map(SemanticTags::getById).filter(Objects::nonNull)
202+
.collect(Collectors.toList());
203+
switch (tags.size()) {
204+
case 0:
205+
case 1:
206+
return true;
207+
case 2:
208+
Class<? extends Tag> firstTag = tags.getFirst();
209+
Class<? extends Tag> lastTag = tags.getLast();
210+
if ((Point.class.isAssignableFrom(firstTag) && Property.class.isAssignableFrom(lastTag))
211+
|| (Point.class.isAssignableFrom(lastTag) && Property.class.isAssignableFrom(firstTag))) {
212+
return true;
213+
}
214+
String firstType = SemanticTags.getSemanticRootName(firstTag);
215+
String lastType = SemanticTags.getSemanticRootName(lastTag);
216+
if (firstType.equals(lastType)) {
217+
if (Point.class.isAssignableFrom(firstTag) || Property.class.isAssignableFrom(firstTag)) {
218+
logger.warn(
219+
"Item '{}' ({}) has an invalid combination of semantic tags: {} ({}) and {} ({}). Only one Point and optionally one Property tag may be assigned.",
220+
item.getName(), semanticType, firstTag.getSimpleName(), firstType,
221+
lastTag.getSimpleName(), lastType);
222+
} else {
223+
logger.warn(
224+
"Item '{}' ({}) has an invalid combination of semantic tags: {} ({}) and {} ({}). Only one {} tag may be assigned.",
225+
item.getName(), semanticType, firstTag.getSimpleName(), firstType,
226+
lastTag.getSimpleName(), lastType, firstType);
227+
}
228+
} else {
229+
logger.warn(
230+
"Item '{}' ({}) has an invalid combination of semantic tags: {} ({}) and {} ({}). {} and {} tags cannot be assigned at the same time.",
231+
item.getName(), semanticType, firstTag.getSimpleName(), firstType, lastTag.getSimpleName(),
232+
lastType, firstType, lastType);
233+
}
234+
return false;
235+
default:
236+
List<String> allTags = tags.stream().map(tag -> {
237+
String tagType = SemanticTags.getSemanticRootName(tag);
238+
return String.format("%s (%s)", tag.getSimpleName(), tagType);
239+
}).toList();
240+
logger.warn(
241+
"Item '{}' ({}) has an invalid combination of semantic tags: {}. An item may only have one tag of Location, Equipment, or Point type. A Property tag may be assigned in conjunction with a Point tag.",
242+
item.getName(), semanticType, allTags);
243+
return false;
244+
}
245+
}
246+
247+
/**
248+
* Verifies the semantics of an item and logs warnings if the semantics are invalid
249+
*
250+
* @param item
251+
* @return true if the semantics are valid, false otherwise
252+
*/
253+
boolean checkSemantics(Item item) {
254+
String itemName = item.getName();
255+
Class<? extends Tag> semanticTag = SemanticTags.getSemanticType(item);
256+
if (semanticTag == null) {
257+
return true;
258+
}
259+
260+
if (!validateTags(item, semanticTag)) {
261+
return false;
262+
}
263+
264+
List<String> warnings = new ArrayList<>();
265+
List<String> parentLocations = new ArrayList<>();
266+
List<String> parentEquipments = new ArrayList<>();
267+
268+
for (String groupName : item.getGroupNames()) {
269+
try {
270+
if (itemRegistry.getItem(groupName) instanceof GroupItem groupItem) {
271+
Class<? extends Tag> groupSemanticType = SemanticTags.getSemanticType(groupItem);
272+
if (groupSemanticType != null) {
273+
if (Equipment.class.isAssignableFrom(groupSemanticType)) {
274+
parentEquipments.add(groupName);
275+
} else if (Location.class.isAssignableFrom(groupSemanticType)) {
276+
parentLocations.add(groupName);
277+
}
278+
}
279+
}
280+
} catch (ItemNotFoundException e) {
281+
// we don't care about invalid parent groups here
282+
}
283+
}
284+
285+
if (Point.class.isAssignableFrom(semanticTag)) {
286+
if (parentLocations.size() == 1 && parentEquipments.size() == 1) {
287+
// This case is allowed: a Point can belong to an Equipment and a Location
288+
//
289+
// Case 1:
290+
// When a location contains multiple equipments -> temperature points,
291+
// the average of the points will be used in the location's UI.
292+
// However, when there is a point which is the direct member of the location,
293+
// it will be used in the location's UI instead of the average.
294+
// So setting one of the equipment's point as a direct member of the location
295+
// allows to override the average.
296+
//
297+
// Case 2:
298+
// When a central Equipment e.g. a HVAC contains Points located in multiple locations,
299+
// e.g. room controls and sensors
300+
String semanticType = SemanticTags.getSemanticRootName(semanticTag);
301+
logger.info("Item '{}' ({}) belongs to location {} and equipment {}.", itemName, semanticType,
302+
parentLocations, parentEquipments);
303+
} else {
304+
if (parentLocations.size() > 1) {
305+
warnings.add(String.format(
306+
"It belongs to multiple locations %s. It should only belong to one Equipment or one location, preferably not both at the same time.",
307+
parentLocations.toString()));
308+
}
309+
if (parentEquipments.size() > 1) {
310+
warnings.add(String.format(
311+
"It belongs to multiple equipments %s. A Point can only belong to at most one Equipment.",
312+
parentEquipments.toString()));
313+
}
314+
}
315+
} else if (Equipment.class.isAssignableFrom(semanticTag)) {
316+
if (parentLocations.size() > 0 && parentEquipments.size() > 0) {
317+
warnings.add(String.format(
318+
"It belongs to location(s) %s and equipment(s) %s. An Equipment can only belong to one Location or another Equipment, but not both.",
319+
parentLocations.toString(), parentEquipments.toString()));
320+
}
321+
if (parentLocations.size() > 1) {
322+
warnings.add(String.format(
323+
"It belongs to multiple locations %s. An Equipment can only belong to one Location or another Equipment.",
324+
parentLocations.toString()));
325+
}
326+
if (parentEquipments.size() > 1) {
327+
warnings.add(String.format(
328+
"It belongs to multiple equipments %s. An Equipment can only belong to at most one Equipment.",
329+
parentEquipments.toString()));
330+
}
331+
} else if (Location.class.isAssignableFrom(semanticTag)) {
332+
if (!(item instanceof GroupItem)) {
333+
warnings.add(String.format("It is a %s item, not a group. A location should be a Group Item.",
334+
item.getType()));
335+
}
336+
if (parentEquipments.size() > 0) {
337+
warnings.add(String.format(
338+
"It belongs to equipment(s) %s. A Location can only belong to another Location, not Equipment.",
339+
parentEquipments.toString()));
340+
}
341+
if (parentLocations.size() > 1) {
342+
warnings.add(
343+
String.format("It belongs to multiple locations %s. It should only belong to one location.",
344+
parentLocations.toString()));
345+
}
346+
}
347+
348+
if (!warnings.isEmpty()) {
349+
String semanticType = SemanticTags.getSemanticRootName(semanticTag);
350+
logger.warn("Item '{}' ({}) has invalid semantic structure: {}", itemName, semanticType,
351+
String.join("\n", warnings));
352+
return false;
353+
}
354+
return true;
355+
}
356+
357+
@Override
358+
public void added(Item item) {
359+
checkSemantics(item);
360+
}
361+
362+
@Override
363+
public void removed(Item item) {
364+
// nothing to do
365+
}
366+
367+
@Override
368+
public void updated(Item oldElement, Item element) {
369+
checkSemantics(element);
370+
}
161371
}

0 commit comments

Comments
 (0)