Added Sematics actions for Rules (#2088)

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
pull/2217/head
Christoph Weitkamp 2021-02-27 10:33:32 +01:00 committed by GitHub
parent 9c0302ec26
commit 191a629e68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 585 additions and 17 deletions

View File

@ -30,6 +30,7 @@ Import-Package: \
org.openhab.core.persistence,\
org.openhab.core.persistence.extensions,\
org.openhab.core.scheduler,\
org.openhab.core.semantics,\
org.openhab.core.thing,\
org.openhab.core.thing.binding,\
org.openhab.core.thing.events,\

View File

@ -35,6 +35,11 @@
<artifactId>org.openhab.core.transform</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.semantics</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.voice</artifactId>

View File

@ -0,0 +1,133 @@
/**
* Copyright (c) 2010-2021 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.script.actions;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.model.script.internal.engine.action.SemanticsActionService;
import org.openhab.core.semantics.model.equipment.CleaningRobot;
import org.openhab.core.semantics.model.location.Bathroom;
import org.openhab.core.semantics.model.location.Indoor;
/**
* This are tests for {@link Semantics} actions.
*
* @author Christoph Weitkamp - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.WARN)
public class SemanticsTest {
private @Mock ItemRegistry mockedItemRegistry;
private GroupItem indoorLocationItem;
private GroupItem bathroomLocationItem;
private GroupItem equipmentItem;
private GenericItem temperaturePointItem;
private GenericItem humidityPointItem;
@BeforeEach
public void setup() throws ItemNotFoundException {
CoreItemFactory itemFactory = new CoreItemFactory();
indoorLocationItem = new GroupItem("TestHouse");
indoorLocationItem.addTag("Indoor");
bathroomLocationItem = new GroupItem("TestBathRoom");
bathroomLocationItem.addTag("Bathroom");
// Bathroom is placed in Indoor
indoorLocationItem.addMember(bathroomLocationItem);
bathroomLocationItem.addGroupName(indoorLocationItem.getName());
equipmentItem = new GroupItem("Test08");
equipmentItem.addTag("CleaningRobot");
// Equipment (Cleaning Robot) is placed in Bathroom
bathroomLocationItem.addMember(equipmentItem);
equipmentItem.addGroupName(bathroomLocationItem.getName());
temperaturePointItem = itemFactory.createItem(CoreItemFactory.NUMBER, "TestTemperature");
temperaturePointItem.addTag("Measurement");
temperaturePointItem.addTag("Temperature");
// Temperature Point is Property of Equipment (Cleaning Robot)
equipmentItem.addMember(temperaturePointItem);
temperaturePointItem.addGroupName(equipmentItem.getName());
humidityPointItem = itemFactory.createItem(CoreItemFactory.NUMBER, "TestHumidity");
humidityPointItem.addTag("Measurement");
humidityPointItem.addTag("Humidity");
when(mockedItemRegistry.getItem("TestHouse")).thenReturn(indoorLocationItem);
when(mockedItemRegistry.getItem("TestBathRoom")).thenReturn(bathroomLocationItem);
when(mockedItemRegistry.getItem("Test08")).thenReturn(equipmentItem);
when(mockedItemRegistry.getItem("TestTemperature")).thenReturn(temperaturePointItem);
when(mockedItemRegistry.getItem("TestHumidity")).thenReturn(humidityPointItem);
new SemanticsActionService(mockedItemRegistry);
}
@Test
public void testGetLocation() {
assertThat(Semantics.getLocation(indoorLocationItem), is(indoorLocationItem));
assertThat(Semantics.getLocation(bathroomLocationItem), is(bathroomLocationItem));
assertThat(Semantics.getLocation(equipmentItem), is(bathroomLocationItem));
assertThat(Semantics.getLocation(temperaturePointItem), is(bathroomLocationItem));
assertNull(Semantics.getLocation(humidityPointItem));
}
@Test
public void testGetLocationType() {
assertThat(Semantics.getLocationType(indoorLocationItem), is(Indoor.class));
assertThat(Semantics.getLocationType(bathroomLocationItem), is(Bathroom.class));
assertNull(Semantics.getLocationType(humidityPointItem));
}
@Test
public void testGetEquipment() {
assertThat(Semantics.getEquipment(equipmentItem), is(equipmentItem));
assertThat(Semantics.getEquipment(temperaturePointItem), is(equipmentItem));
assertNull(Semantics.getEquipment(humidityPointItem));
}
@Test
public void testGetEquipmentType() {
assertThat(Semantics.getEquipmentType(equipmentItem), is(CleaningRobot.class));
assertThat(Semantics.getEquipmentType(temperaturePointItem), is(CleaningRobot.class));
assertNull(Semantics.getEquipmentType(humidityPointItem));
}
}

View File

@ -0,0 +1,161 @@
/**
* Copyright (c) 2010-2021 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.script.actions;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.Item;
import org.openhab.core.model.script.engine.action.ActionDoc;
import org.openhab.core.model.script.internal.engine.action.SemanticsActionService;
import org.openhab.core.semantics.SemanticTags;
import org.openhab.core.semantics.model.Equipment;
import org.openhab.core.semantics.model.Location;
import org.openhab.core.semantics.model.Point;
import org.openhab.core.semantics.model.Property;
import org.openhab.core.semantics.model.Tag;
/**
* The static methods of this class are made available as functions in the scripts. This allows a script to use
* Semantics features.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public class Semantics {
/**
* Checks if the given {@link Item} is a {@link Location}.
*
* @param item the Item to check
* @return return true, if the given Item is a Location, false otherwise
*/
@ActionDoc(text = "checks if the given Item is is a Location")
public static boolean isLocation(Item item) {
return SemanticsActionService.isLocation(item);
}
/**
* Checks if the given {@link Item} is a {@link Equipment}.
*
* @param item the Item to check
* @return return true, if the given Item is an Equipment, false otherwise
*/
@ActionDoc(text = "checks if the given Item is is an Equipment")
public static boolean isEquipment(Item item) {
return SemanticsActionService.isEquipment(item);
}
/**
* Checks if the given {@link Item} is a {@link Point}.
*
* @param item the Item to check
* @return return true, if the given Item is a Point, false otherwise
*/
@ActionDoc(text = "checks if the given Item is is a Point")
public static boolean isPoint(Item item) {
return SemanticsActionService.isPoint(item);
}
/**
* Gets the related {@link Location} Item of an {@link Item}.
*
* @param item the Item to determine the Location for
* @return the related Location Item of the Item or null
*/
@ActionDoc(text = "gets the Location Item of the Item")
public static @Nullable Item getLocation(Item item) {
if (isLocation(item)) {
// if item is a location, return itself
return item;
} else {
// if item is not a location, iterate its groups and try to determine a location from them
return SemanticsActionService.getLocationItemFromGroupNames(item.getGroupNames());
}
}
/**
* Gets the related {@link Location} type of an {@link Item}.
*
* @param item the Item to determine the Location for
* @return the related Location type of the Item or null
*/
@ActionDoc(text = "gets the Location type of the Item")
public static @Nullable Class<? extends Location> getLocationType(Item item) {
Item locationItem = getLocation(item);
return locationItem != null ? SemanticTags.getLocation(locationItem) : null;
}
/**
* Gets the related {@link Equipment} Item an {@link Item} belongs to.
*
* @param item the Item to retrieve the Equipment Item for
* @return the related Equipment Item the Item belongs to or null
*/
@ActionDoc(text = "gets the Equipment Item an Item belongs to")
public static @Nullable Item getEquipment(Item item) {
if (isEquipment(item)) {
// if item is an equipment return its semantics equipment class
return item;
} else {
// if item is not an equipment, iterate its groups and try to determine a equipment there
return SemanticsActionService.getEquipmentItemFromGroupNames(item.getGroupNames());
}
}
/**
* Gets the {@link Equipment} type an {@link Item} relates to.
*
* @param item the Item to retrieve the Equipment for
* @return the Equipment the Item relates to or null
*/
@ActionDoc(text = "gets the Equipment type an Item belongs to")
public static @Nullable Class<? extends Equipment> getEquipmentType(Item item) {
Item equipmentItem = getEquipment(item);
return equipmentItem != null ? SemanticTags.getEquipment(equipmentItem) : null;
}
/**
* Gets the {@link Point} type an {@link Item}.
*
* @param item the Item to determine the Point for
* @return the Point type of the Item or null
*/
@ActionDoc(text = "gets the Point type of an Item")
public static @Nullable Class<? extends Point> getPointType(Item item) {
return isPoint(item) ? SemanticTags.getPoint(item) : null;
}
/**
* Gets the {@link Property} type an {@link Item} relates to.
*
* @param item the Item to retrieve the Property for
* @return the Property type the Item relates to or null
*/
@ActionDoc(text = "gets the Property type an Item relates to")
public static @Nullable Class<? extends Property> getPropertyType(Item item) {
return isPoint(item) ? SemanticTags.getProperty(item) : null;
}
/**
* Determines the semantic type of an {@link Item} (i.e. a sub-type of {@link Location}, {@link Equipment} or
* {@link Point}).
*
* @param item the Item to get the semantic type for
* @return a sub-type of Location, Equipment or Point
*/
@ActionDoc(text = "gets the semantic type of an Item")
public static @Nullable Class<? extends Tag> getSemanticType(Item item) {
return SemanticTags.getSemanticType(item);
}
}

View File

@ -0,0 +1,116 @@
/**
* Copyright (c) 2010-2021 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.script.internal.engine.action;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.model.script.actions.Semantics;
import org.openhab.core.model.script.engine.action.ActionService;
import org.openhab.core.semantics.SemanticsPredicates;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* This class registers an OSGi service for the Semantics action.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
@Component
public class SemanticsActionService implements ActionService {
private static @Nullable ItemRegistry itemRegistry;
@Activate
public SemanticsActionService(final @Reference ItemRegistry itemRegistry) {
SemanticsActionService.itemRegistry = itemRegistry;
}
@Override
public Class<?> getActionClass() {
return Semantics.class;
}
public static boolean isLocation(Item item) {
return SemanticsPredicates.isLocation().test(item);
}
public static boolean isEquipment(Item item) {
return SemanticsPredicates.isEquipment().test(item);
}
public static boolean isPoint(Item item) {
return SemanticsPredicates.isPoint().test(item);
}
public static @Nullable Item getLocationItemFromGroupNames(List<String> groupNames) {
ItemRegistry ir = itemRegistry;
if (ir != null) {
List<Item> groupItems = new ArrayList<>();
for (String groupName : groupNames) {
try {
Item group = ir.getItem(groupName);
// if group is a location, return it (first location found)
if (isLocation(group)) {
return group;
}
groupItems.add(group);
} catch (ItemNotFoundException e) {
// should not happen
}
}
// if no location is found, iterate the groups of each group
for (Item group : groupItems) {
Item locationItem = getLocationItemFromGroupNames(group.getGroupNames());
if (locationItem != null) {
return locationItem;
}
}
}
return null;
}
public static @Nullable Item getEquipmentItemFromGroupNames(List<String> groupNames) {
ItemRegistry ir = itemRegistry;
if (ir != null) {
List<Item> groupItems = new ArrayList<>();
for (String groupName : groupNames) {
try {
Item group = ir.getItem(groupName);
// if group is an equipment, return it (first equipment found)
if (isEquipment(group)) {
return group;
}
groupItems.add(group);
} catch (ItemNotFoundException e) {
// should not happen
}
}
// if no equipment is found, iterate the groups of each group
for (Item group : groupItems) {
Item equipmentItem = getEquipmentItemFromGroupNames(group.getGroupNames());
if (equipmentItem != null) {
return equipmentItem;
}
}
}
return null;
}
}

View File

@ -26,6 +26,9 @@ import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.Item;
import org.openhab.core.semantics.model.Equipment;
import org.openhab.core.semantics.model.Location;
import org.openhab.core.semantics.model.Point;
import org.openhab.core.semantics.model.Property;
import org.openhab.core.semantics.model.Tag;
import org.openhab.core.semantics.model.TagInfo;
@ -107,14 +110,14 @@ public class SemanticTags {
}
/**
* Determines the semantic entity type of an item, i.e. a sub-type of Location, Equipment or Point.
* Determines the semantic type of an {@link Item} i.e. a sub-type of {@link Location}, {@link Equipment} or
* {@link Point}.
*
* @param item the item to get the semantic type for
* @param item the Item to get the semantic type for
* @return a sub-type of Location, Equipment or Point
*/
public static @Nullable Class<? extends Tag> getSemanticType(Item item) {
Set<String> tags = item.getTags();
for (String tag : tags) {
for (String tag : item.getTags()) {
Class<? extends Tag> type = getById(tag);
if (type != null && !Property.class.isAssignableFrom(type)) {
return type;
@ -134,15 +137,14 @@ public class SemanticTags {
}
/**
* Determines the Property that a Point relates to.
* Determines the {@link Property} type that a {@link Point} relates to.
*
* @param item the item to get the property for
* @return a sub-type of Property if the item represents a Point, otherwise null
* @param item the Item to get the property for
* @return a sub-type of Property if the Item represents a Point, otherwise null
*/
@SuppressWarnings("unchecked")
public static @Nullable Class<? extends Property> getProperty(Item item) {
Set<String> tags = item.getTags();
for (String tag : tags) {
for (String tag : item.getTags()) {
Class<? extends Tag> type = getById(tag);
if (type != null && Property.class.isAssignableFrom(type)) {
return (Class<? extends Property>) type;
@ -151,6 +153,58 @@ public class SemanticTags {
return null;
}
/**
* Determines the semantic {@link Point} type of an {@link Item}.
*
* @param item the Item to get the Point for
* @return a sub-type of a {@link Point}if the Item represents an Point, otherwise null
*/
@SuppressWarnings("unchecked")
public static @Nullable Class<? extends Point> getPoint(Item item) {
Set<String> tags = item.getTags();
for (String tag : tags) {
Class<? extends Tag> type = getById(tag);
if (type != null && Point.class.isAssignableFrom(type)) {
return (Class<? extends Point>) type;
}
}
return null;
}
/**
* Determines the semantic {@link Equipment} type of an {@link Item}.
*
* @param item the Item to get the Equipment for
* @return a sub-type of {@link Equipment} if the Item represents an Equipment, otherwise null
*/
@SuppressWarnings("unchecked")
public static @Nullable Class<? extends Equipment> getEquipment(Item item) {
for (String tag : item.getTags()) {
Class<? extends Tag> type = getById(tag);
if (type != null && Equipment.class.isAssignableFrom(type)) {
return (Class<? extends Equipment>) type;
}
}
return null;
}
/**
* Determines the semantic {@link Location} type of an {@link Item}.
*
* @param item the item to get the location for
* @return a sub-type of {@link Location} if the item represents a location, otherwise null
*/
@SuppressWarnings("unchecked")
public static @Nullable Class<? extends Location> getLocation(Item item) {
for (String tag : item.getTags()) {
Class<? extends Tag> type = getById(tag);
if (type != null && Location.class.isAssignableFrom(type)) {
return (Class<? extends Location>) type;
}
}
return null;
}
private static void addTagSet(Class<? extends Tag> tagSet) {
String id = tagSet.getAnnotation(TagInfo.class).id();
while (id.indexOf("_") != -1) {

View File

@ -39,8 +39,9 @@ public class SemanticTagsTest {
private GenericItem pointItem;
@BeforeEach
public void setup() throws Exception {
public void setup() {
CoreItemFactory itemFactory = new CoreItemFactory();
locationItem = new GroupItem("TestBathRoom");
locationItem.addTag("Bathroom");
@ -84,6 +85,21 @@ public class SemanticTagsTest {
assertEquals(Measurement.class, SemanticTags.getSemanticType(pointItem));
}
@Test
public void testGetLocation() {
assertEquals(Bathroom.class, SemanticTags.getLocation(locationItem));
}
@Test
public void testGetEquipment() {
assertEquals(CleaningRobot.class, SemanticTags.getEquipment(equipmentItem));
}
@Test
public void testGetPoint() {
assertEquals(Measurement.class, SemanticTags.getPoint(pointItem));
}
@Test
public void testGetProperty() {
assertEquals(Temperature.class, SemanticTags.getProperty(pointItem));

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2021 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.semantics;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.semantics.model.property.Humidity;
import org.openhab.core.semantics.model.property.Temperature;
/**
* This are tests for {@link SemanticsPredicates}.
*
* @author Christoph Weitkamp - Initial contribution
*/
public class SemanticsPredicatesTest {
private GroupItem locationItem;
private GroupItem equipmentItem;
private GenericItem pointItem;
@BeforeEach
public void setup() {
CoreItemFactory itemFactory = new CoreItemFactory();
locationItem = new GroupItem("TestBathRoom");
locationItem.addTag("Bathroom");
equipmentItem = new GroupItem("Test08");
equipmentItem.addTag("CleaningRobot");
pointItem = itemFactory.createItem(CoreItemFactory.NUMBER, "TestTemperature");
pointItem.addTag("Measurement");
pointItem.addTag("Temperature");
}
@Test
public void testIsLocation() {
assertTrue(SemanticsPredicates.isLocation().test(locationItem));
assertFalse(SemanticsPredicates.isLocation().test(equipmentItem));
assertFalse(SemanticsPredicates.isLocation().test(pointItem));
}
@Test
public void testIsEquipment() {
assertFalse(SemanticsPredicates.isEquipment().test(locationItem));
assertTrue(SemanticsPredicates.isEquipment().test(equipmentItem));
assertFalse(SemanticsPredicates.isEquipment().test(pointItem));
}
@Test
public void testIsPoint() {
assertFalse(SemanticsPredicates.isPoint().test(locationItem));
assertFalse(SemanticsPredicates.isPoint().test(equipmentItem));
assertTrue(SemanticsPredicates.isPoint().test(pointItem));
}
@Test
public void testRelatesTo() {
assertFalse(SemanticsPredicates.relatesTo(Temperature.class).test(locationItem));
assertFalse(SemanticsPredicates.relatesTo(Temperature.class).test(equipmentItem));
assertTrue(SemanticsPredicates.relatesTo(Temperature.class).test(pointItem));
assertFalse(SemanticsPredicates.relatesTo(Humidity.class).test(equipmentItem));
}
}

View File

@ -104,4 +104,5 @@ Fragment-Host: org.openhab.core.model.core
org.objenesis;version='[3.1.0,3.1.1)',\
biz.aQute.tester.junit-platform;version='[5.2.0,5.2.1)',\
org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\
org.openhab.core.model.persistence.runtime;version='[3.1.0,3.1.1)'
org.openhab.core.model.persistence.runtime;version='[3.1.0,3.1.1)',\
org.openhab.core.semantics;version='[3.1.0,3.1.1)'

View File

@ -102,4 +102,5 @@ Fragment-Host: org.openhab.core.model.item
junit-platform-launcher;version='[1.7.0,1.7.1)',\
biz.aQute.tester.junit-platform;version='[5.2.0,5.2.1)',\
org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\
org.openhab.core.model.persistence.runtime;version='[3.1.0,3.1.1)'
org.openhab.core.model.persistence.runtime;version='[3.1.0,3.1.1)',\
org.openhab.core.semantics;version='[3.1.0,3.1.1)'

View File

@ -107,5 +107,6 @@ Fragment-Host: org.openhab.core.model.rule.runtime
junit-platform-launcher;version='[1.7.0,1.7.1)',\
biz.aQute.tester.junit-platform;version='[5.2.0,5.2.1)',\
org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\
org.openhab.core.model.persistence.runtime;version='[3.1.0,3.1.1)'
org.openhab.core.model.persistence.runtime;version='[3.1.0,3.1.1)',\
org.openhab.core.semantics;version='[3.1.0,3.1.1)'
-runblacklist: bnd.identity;id='jakarta.activation-api'

View File

@ -101,5 +101,5 @@ Fragment-Host: org.openhab.core.model.script
junit-platform-launcher;version='[1.7.0,1.7.1)',\
biz.aQute.tester.junit-platform;version='[5.2.0,5.2.1)',\
org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\
org.openhab.core.model.persistence.runtime;version='[3.1.0,3.1.1)'
org.openhab.core.model.persistence.runtime;version='[3.1.0,3.1.1)',\
org.openhab.core.semantics;version='[3.1.0,3.1.1)'

View File

@ -115,5 +115,5 @@ Fragment-Host: org.openhab.core.model.thing
org.objenesis;version='[3.1.0,3.1.1)',\
biz.aQute.tester.junit-platform;version='[5.2.0,5.2.1)',\
org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\
org.openhab.core.model.persistence.runtime;version='[3.1.0,3.1.1)'
org.openhab.core.model.persistence.runtime;version='[3.1.0,3.1.1)',\
org.openhab.core.semantics;version='[3.1.0,3.1.1)'