17
17
import java .util .HashSet ;
18
18
import java .util .List ;
19
19
import java .util .Locale ;
20
+ import java .util .Objects ;
20
21
import java .util .Optional ;
21
22
import java .util .Set ;
22
23
import java .util .function .Predicate ;
25
26
26
27
import org .eclipse .jdt .annotation .NonNullByDefault ;
27
28
import org .eclipse .jdt .annotation .Nullable ;
29
+ import org .openhab .core .common .registry .RegistryChangeListener ;
28
30
import org .openhab .core .items .GroupItem ;
29
31
import org .openhab .core .items .Item ;
32
+ import org .openhab .core .items .ItemNotFoundException ;
30
33
import org .openhab .core .items .ItemPredicates ;
31
34
import org .openhab .core .items .ItemRegistry ;
32
35
import org .openhab .core .items .Metadata ;
35
38
import org .openhab .core .semantics .Equipment ;
36
39
import org .openhab .core .semantics .Location ;
37
40
import org .openhab .core .semantics .Point ;
41
+ import org .openhab .core .semantics .Property ;
38
42
import org .openhab .core .semantics .SemanticTag ;
39
43
import org .openhab .core .semantics .SemanticTagRegistry ;
44
+ import org .openhab .core .semantics .SemanticTags ;
40
45
import org .openhab .core .semantics .SemanticsPredicates ;
41
46
import org .openhab .core .semantics .SemanticsService ;
42
47
import org .openhab .core .semantics .Tag ;
43
48
import org .osgi .service .component .annotations .Activate ;
44
49
import org .osgi .service .component .annotations .Component ;
50
+ import org .osgi .service .component .annotations .Deactivate ;
45
51
import org .osgi .service .component .annotations .Reference ;
46
52
47
53
/**
48
54
* The internal implementation of the {@link SemanticsService} interface, which is registered as an OSGi service.
49
55
*
50
56
* @author Kai Kreuzer - Initial contribution
51
57
* @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
52
59
*/
53
60
@ NonNullByDefault
54
- @ Component
55
- public class SemanticsServiceImpl implements SemanticsService {
61
+ @ Component ( immediate = true )
62
+ public class SemanticsServiceImpl implements SemanticsService , RegistryChangeListener < Item > {
56
63
57
64
private static final String SYNONYMS_NAMESPACE = "synonyms" ;
58
65
66
+ private static final org .slf4j .Logger logger = org .slf4j .LoggerFactory .getLogger (SemanticsServiceImpl .class );
67
+
59
68
private final ItemRegistry itemRegistry ;
60
69
private final MetadataRegistry metadataRegistry ;
61
70
private final SemanticTagRegistry semanticTagRegistry ;
@@ -67,6 +76,14 @@ public SemanticsServiceImpl(final @Reference ItemRegistry itemRegistry,
67
76
this .itemRegistry = itemRegistry ;
68
77
this .metadataRegistry = metadataRegistry ;
69
78
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 );
70
87
}
71
88
72
89
@ Override
@@ -158,4 +175,197 @@ private List<String> getLabelAndSynonyms(SemanticTag tag, Locale locale) {
158
175
Stream <String > synonyms = localizedTag .getSynonyms ().stream ();
159
176
return Stream .concat (label , synonyms ).map (s -> s .toLowerCase (locale )).distinct ().toList ();
160
177
}
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
+ }
161
371
}
0 commit comments