[uom] Add unit metadata for NumberItem (#3481)

* Add defaultUnit metadata for NumberItem

Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
J-N-K 2023-05-09 22:42:25 +02:00 committed by GitHub
parent 67b80af872
commit 9ef076dc6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 941 additions and 716 deletions

View File

@ -13,6 +13,7 @@
package org.openhab.core.automation.internal.module.handler;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.time.ZoneId;
@ -20,6 +21,8 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -34,6 +37,7 @@ import org.openhab.core.automation.Condition;
import org.openhab.core.automation.util.ConditionBuilder;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
@ -77,7 +81,9 @@ public class ItemStateConditionHandlerTest extends JavaTest {
((NumberItem) item).setState(itemState);
break;
case "Number:Temperature":
item = new NumberItem("Number:Temperature", ITEM_NAME);
UnitProvider unitProviderMock = mock(UnitProvider.class);
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
item = new NumberItem("Number:Temperature", ITEM_NAME, unitProviderMock);
((NumberItem) item).setState(itemState);
break;
case "Dimmer":
@ -102,8 +108,8 @@ public class ItemStateConditionHandlerTest extends JavaTest {
{ new ParameterSet("Number", "5", new DecimalType(23), false) }, //
{ new ParameterSet("Number", "5", new DecimalType(5), true) }, //
{ new ParameterSet("Number:Temperature", "5 °C", new DecimalType(23), false) }, //
{ new ParameterSet("Number:Temperature", "5 °C", new DecimalType(5), false) }, //
{ new ParameterSet("Number:Temperature", "0", new QuantityType<>(), true) }, //
{ new ParameterSet("Number:Temperature", "5 °C", new DecimalType(5), true) }, //
{ new ParameterSet("Number:Temperature", "0", new DecimalType(), false) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(23, SIUnits.CELSIUS), false) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, SIUnits.CELSIUS), false) }, //
{ new ParameterSet("Number:Temperature", "5 °C", new QuantityType<>(23, SIUnits.CELSIUS), false) }, //
@ -119,7 +125,7 @@ public class ItemStateConditionHandlerTest extends JavaTest {
{ new ParameterSet("Number", "5", new DecimalType(5), false) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(23), true) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(5), false) }, //
{ new ParameterSet("Number:Temperature", "0", new QuantityType<>(), false) }, //
{ new ParameterSet("Number:Temperature", "0", new DecimalType(), false) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(23, SIUnits.CELSIUS), true) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, SIUnits.CELSIUS), false) }, //
{ new ParameterSet("Number:Temperature", "5 °C", new QuantityType<>(23, SIUnits.CELSIUS), true) }, //
@ -138,7 +144,7 @@ public class ItemStateConditionHandlerTest extends JavaTest {
{ new ParameterSet("Number", "5 °C", new DecimalType(23), true) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(5), true) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(4), false) }, //
{ new ParameterSet("Number:Temperature", "0", new QuantityType<>(), true) }, //
{ new ParameterSet("Number:Temperature", "0", new DecimalType(), true) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(23, SIUnits.CELSIUS), true) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, SIUnits.CELSIUS), true) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(4, SIUnits.CELSIUS), false) }, //
@ -159,7 +165,7 @@ public class ItemStateConditionHandlerTest extends JavaTest {
{ new ParameterSet("Number", "5", new DecimalType(4), true) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(23), false) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(4), true) }, //
{ new ParameterSet("Number:Temperature", "0", new QuantityType<>(), false) }, //
{ new ParameterSet("Number:Temperature", "0", new DecimalType(), false) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(23, SIUnits.CELSIUS), false) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(4, SIUnits.CELSIUS), true) }, //
{ new ParameterSet("Number:Temperature", "5 °C", new QuantityType<>(23, SIUnits.CELSIUS), false) }, //
@ -179,7 +185,7 @@ public class ItemStateConditionHandlerTest extends JavaTest {
{ new ParameterSet("Number", "5 °C", new DecimalType(23), false) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(5), true) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(4), true) }, //
{ new ParameterSet("Number:Temperature", "0", new QuantityType<>(), true) }, //
{ new ParameterSet("Number:Temperature", "0", new DecimalType(), true) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(23, SIUnits.CELSIUS), false) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, SIUnits.CELSIUS), true) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(4, SIUnits.CELSIUS), true) }, //
@ -220,9 +226,11 @@ public class ItemStateConditionHandlerTest extends JavaTest {
ItemStateConditionHandler handler = initItemStateConditionHandler("=", parameterSet.comparisonState);
if (parameterSet.expectedResult) {
assertTrue(handler.isSatisfied(Map.of()));
assertTrue(handler.isSatisfied(Map.of()),
parameterSet.item + ", comparisonState=" + parameterSet.comparisonState);
} else {
assertFalse(handler.isSatisfied(Map.of()));
assertFalse(handler.isSatisfied(Map.of()),
parameterSet.item + ", comparisonState=" + parameterSet.comparisonState);
}
}

View File

@ -15,10 +15,12 @@ package org.openhab.core.io.rest.core.item;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.Mockito.mock;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem;
import org.openhab.core.library.CoreItemFactory;
@ -38,7 +40,7 @@ public class EnrichedItemDTOMapperTest extends JavaTest {
@Test
public void testFiltering() {
CoreItemFactory itemFactory = new CoreItemFactory();
CoreItemFactory itemFactory = new CoreItemFactory(mock(UnitProvider.class));
GroupItem group = new GroupItem("TestGroup");
GroupItem subGroup = new GroupItem("TestSubGroup");

View File

@ -18,6 +18,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -25,6 +26,7 @@ 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.i18n.UnitProvider;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.ItemNotFoundException;
@ -43,20 +45,22 @@ import org.openhab.core.semantics.model.location.Indoor;
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class SemanticsTest {
private @Mock ItemRegistry mockedItemRegistry;
private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock;
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
private GroupItem indoorLocationItem;
private GroupItem bathroomLocationItem;
private GroupItem equipmentItem;
private GenericItem temperaturePointItem;
private GenericItem humidityPointItem;
private GenericItem subEquipmentItem;
private @NonNullByDefault({}) GroupItem indoorLocationItem;
private @NonNullByDefault({}) GroupItem bathroomLocationItem;
private @NonNullByDefault({}) GroupItem equipmentItem;
private @NonNullByDefault({}) GenericItem temperaturePointItem;
private @NonNullByDefault({}) GenericItem humidityPointItem;
private @NonNullByDefault({}) GenericItem subEquipmentItem;
@BeforeEach
public void setup() throws ItemNotFoundException {
CoreItemFactory itemFactory = new CoreItemFactory();
CoreItemFactory itemFactory = new CoreItemFactory(unitProviderMock);
indoorLocationItem = new GroupItem("TestHouse");
indoorLocationItem.addTag("Indoor");
@ -94,13 +98,13 @@ public class SemanticsTest {
equipmentItem.addMember(subEquipmentItem);
subEquipmentItem.addGroupName(equipmentItem.getName());
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);
when(itemRegistryMock.getItem("TestHouse")).thenReturn(indoorLocationItem);
when(itemRegistryMock.getItem("TestBathRoom")).thenReturn(bathroomLocationItem);
when(itemRegistryMock.getItem("Test08")).thenReturn(equipmentItem);
when(itemRegistryMock.getItem("TestTemperature")).thenReturn(temperaturePointItem);
when(itemRegistryMock.getItem("TestHumidity")).thenReturn(humidityPointItem);
new SemanticsActionService(mockedItemRegistry);
new SemanticsActionService(itemRegistryMock);
}
@Test

View File

@ -82,11 +82,10 @@ public class PersistenceExtensionsTest {
public void setUp() {
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
CoreItemFactory itemFactory = new CoreItemFactory();
CoreItemFactory itemFactory = new CoreItemFactory(unitProviderMock);
numberItem = itemFactory.createItem(CoreItemFactory.NUMBER, TEST_NUMBER);
quantityItem = itemFactory.createItem(CoreItemFactory.NUMBER + ItemUtil.EXTENSION_SEPARATOR + "Temperature",
TEST_QUANTITY_NUMBER);
quantityItem.setUnitProvider(unitProviderMock);
switchItem = itemFactory.createItem(CoreItemFactory.SWITCH, TEST_SWITCH);
when(itemRegistryMock.get(TEST_NUMBER)).thenReturn(numberItem);

View File

@ -15,12 +15,14 @@ package org.openhab.core.semantics;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem;
import org.openhab.core.library.CoreItemFactory;
@ -49,7 +51,7 @@ public class SemanticTagsTest {
@BeforeEach
public void setup() {
CoreItemFactory itemFactory = new CoreItemFactory();
CoreItemFactory itemFactory = new CoreItemFactory(mock(UnitProvider.class));
locationItem = new GroupItem("TestBathRoom");
locationItem.addTag("Bathroom");

View File

@ -13,10 +13,12 @@
package org.openhab.core.semantics;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem;
import org.openhab.core.library.CoreItemFactory;
@ -24,7 +26,7 @@ import org.openhab.core.semantics.model.property.Humidity;
import org.openhab.core.semantics.model.property.Temperature;
/**
* This are tests for {@link SemanticsPredicates}.
* These are tests for {@link SemanticsPredicates}.
*
* @author Christoph Weitkamp - Initial contribution
*/
@ -37,7 +39,7 @@ public class SemanticsPredicatesTest {
@BeforeEach
public void setup() {
CoreItemFactory itemFactory = new CoreItemFactory();
CoreItemFactory itemFactory = new CoreItemFactory(mock(UnitProvider.class));
locationItem = new GroupItem("TestBathRoom");
locationItem.addTag("Bathroom");

View File

@ -25,6 +25,7 @@ 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.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
@ -43,6 +44,7 @@ public class SemanticsServiceImplTest {
private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock;
private @Mock @NonNullByDefault({}) MetadataRegistry metadataRegistryMock;
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
private @NonNullByDefault({}) GroupItem locationItem;
private @NonNullByDefault({}) GroupItem equipmentItem;
@ -52,7 +54,7 @@ public class SemanticsServiceImplTest {
@BeforeEach
public void setup() throws Exception {
CoreItemFactory itemFactory = new CoreItemFactory();
CoreItemFactory itemFactory = new CoreItemFactory(unitProviderMock);
locationItem = new GroupItem("TestBathRoom");
locationItem.addTag("Bathroom");
locationItem.setLabel("Joe's Room");

View File

@ -24,7 +24,7 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.measure.Quantity;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -35,6 +35,7 @@ import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.events.EventSubscriber;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemFactory;
import org.openhab.core.items.ItemRegistry;
@ -44,8 +45,10 @@ import org.openhab.core.items.events.AbstractItemRegistryEvent;
import org.openhab.core.items.events.GroupStateUpdatedEvent;
import org.openhab.core.items.events.ItemCommandEvent;
import org.openhab.core.items.events.ItemStateUpdatedEvent;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
@ -70,13 +73,10 @@ import org.openhab.core.thing.profiles.ProfileFactory;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.thing.profiles.StateProfile;
import org.openhab.core.thing.profiles.TriggerProfile;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.util.UnitUtils;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
@ -131,6 +131,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
private final EventPublisher eventPublisher;
private final SafeCaller safeCaller;
private final ThingRegistry thingRegistry;
private final UnitProvider unitProvider;
private final ExpiringCacheMap<Integer, Profile> profileSafeCallCache = new ExpiringCacheMap<>(CACHE_EXPIRATION);
@ -143,7 +144,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
final @Reference ItemStateConverter itemStateConverter, //
final @Reference EventPublisher eventPublisher, //
final @Reference SafeCaller safeCaller, //
final @Reference ThingRegistry thingRegistry) {
final @Reference ThingRegistry thingRegistry, final @Reference UnitProvider unitProvider) {
this.autoUpdateManager = autoUpdateManager;
this.channelTypeRegistry = channelTypeRegistry;
this.defaultProfileFactory = defaultProfileFactory;
@ -153,6 +154,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
this.eventPublisher = eventPublisher;
this.safeCaller = safeCaller;
this.thingRegistry = thingRegistry;
this.unitProvider = unitProvider;
itemChannelLinkRegistry.addRegistryChangeListener(this);
}
@ -203,10 +205,6 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
}
}
private @Nullable Thing getThing(ThingUID thingUID) {
return thingRegistry.get(thingUID);
}
private Profile getProfile(ItemChannelLink link, Item item, @Nullable Thing thing) {
synchronized (profiles) {
Profile profile = profiles.get(link.getUID());
@ -228,8 +226,8 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
}
private ProfileCallback createCallback(ItemChannelLink link) {
return new ProfileCallbackImpl(eventPublisher, safeCaller, itemStateConverter, link,
thingUID -> getThing(thingUID), itemName -> getItem(itemName));
return new ProfileCallbackImpl(eventPublisher, safeCaller, itemStateConverter, link, thingRegistry::get,
this::getItem);
}
private @Nullable ProfileTypeUID determineProfileTypeUID(ItemChannelLink link, Item item, @Nullable Thing thing) {
@ -272,25 +270,21 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
String profileName = (String) link.getConfiguration()
.get(ItemChannelLinkConfigDescriptionProvider.PARAM_PROFILE);
if (profileName != null && !profileName.trim().isEmpty()) {
profileName = normalizeProfileName(profileName);
if (!profileName.contains(AbstractUID.SEPARATOR)) {
profileName = ProfileTypeUID.SYSTEM_SCOPE + AbstractUID.SEPARATOR + profileName;
}
return new ProfileTypeUID(profileName);
}
return null;
}
private String normalizeProfileName(String profileName) {
if (!profileName.contains(AbstractUID.SEPARATOR)) {
return ProfileTypeUID.SYSTEM_SCOPE + AbstractUID.SEPARATOR + profileName;
}
return profileName;
}
private @Nullable Profile getProfileFromFactories(ProfileTypeUID profileTypeUID, ItemChannelLink link,
ProfileCallback callback) {
ProfileContext context = null;
Item item = getItem(link.getItemName());
Thing thing = getThing(link.getLinkedUID().getThingUID());
ThingUID thingUID = link.getLinkedUID().getThingUID();
Thing thing = thingRegistry.get(thingUID);
if (item != null && thing != null) {
Channel channel = thing.getChannel(link.getLinkedUID());
if (channel != null) {
@ -341,48 +335,51 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
autoUpdateManager.receiveCommand(commandEvent, item);
}
handleEvent(itemName, command, commandEvent.getSource(), s -> acceptedCommandTypeMap.get(s),
(profile, thing, convertedCommand) -> {
if (profile instanceof StateProfile stateProfile) {
int key = Objects.hash("COMMAND", profile, thing);
Profile p = profileSafeCallCache.putIfAbsentAndGet(key,
() -> safeCaller.create(stateProfile, StateProfile.class) //
.withAsync() //
.withIdentifier(thing) //
.withTimeout(THINGHANDLER_EVENT_TIMEOUT) //
.build());
if (p instanceof StateProfile profileP) {
profileP.onCommandFromItem(convertedCommand);
} else {
throw new IllegalStateException("ExpiringCache didn't provide a StateProfile instance!");
}
}
});
handleEvent(itemName, command, commandEvent.getSource(), acceptedCommandTypeMap::get,
this::applyProfileForCommand);
}
private void receiveUpdate(ItemStateUpdatedEvent updateEvent) {
final String itemName = updateEvent.getItemName();
final State newState = updateEvent.getItemState();
handleEvent(itemName, newState, updateEvent.getSource(), s -> acceptedStateTypeMap.get(s),
(profile, thing, convertedState) -> {
int key = Objects.hash("UPDATE", profile, thing);
Profile p = profileSafeCallCache.putIfAbsentAndGet(key,
() -> safeCaller.create(profile, Profile.class) //
.withAsync() //
.withIdentifier(thing) //
.withTimeout(THINGHANDLER_EVENT_TIMEOUT) //
.build());
if (p != null) {
p.onStateUpdateFromItem(convertedState);
} else {
throw new IllegalStateException("ExpiringCache didn't provide a Profile instance!");
}
});
handleEvent(itemName, newState, updateEvent.getSource(), acceptedStateTypeMap::get,
this::applyProfileForUpdate);
}
@FunctionalInterface
private static interface ProfileAction<T extends Type> {
void handle(Profile profile, Thing thing, T type);
private interface ProfileAction<T extends Type> {
void applyProfile(Profile profile, Thing thing, T type);
}
private void applyProfileForUpdate(Profile profile, Thing thing, State convertedState) {
int key = Objects.hash("UPDATE", profile, thing);
Profile p = profileSafeCallCache.putIfAbsentAndGet(key, () -> safeCaller.create(profile, Profile.class) //
.withAsync() //
.withIdentifier(thing) //
.withTimeout(THINGHANDLER_EVENT_TIMEOUT) //
.build());
if (p != null) {
p.onStateUpdateFromItem(convertedState);
} else {
throw new IllegalStateException("ExpiringCache didn't provide a Profile instance!");
}
}
private void applyProfileForCommand(Profile profile, Thing thing, Command convertedCommand) {
if (profile instanceof StateProfile stateProfile) {
int key = Objects.hash("COMMAND", profile, thing);
Profile p = profileSafeCallCache.putIfAbsentAndGet(key,
() -> safeCaller.create(stateProfile, StateProfile.class) //
.withAsync() //
.withIdentifier(thing) //
.withTimeout(THINGHANDLER_EVENT_TIMEOUT) //
.build());
if (p instanceof StateProfile profileP) {
profileP.onCommandFromItem(convertedCommand);
} else {
throw new IllegalStateException("ExpiringCache didn't provide a StateProfile instance!");
}
}
}
private <T extends Type> void handleEvent(String itemName, T type, @Nullable String source,
@ -399,7 +396,8 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
return !link.getLinkedUID().toString().equals(source);
}).forEach(link -> {
ChannelUID channelUID = link.getLinkedUID();
Thing thing = getThing(channelUID.getThingUID());
ThingUID thingUID = channelUID.getThingUID();
Thing thing = thingRegistry.get(thingUID);
if (thing != null) {
Channel channel = thing.getChannel(channelUID.getId());
if (channel != null) {
@ -408,7 +406,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
if (convertedType != null) {
if (thing.getHandler() != null) {
Profile profile = getProfile(link, item, thing);
action.handle(profile, thing, convertedType);
action.applyProfile(profile, thing, convertedType);
}
} else {
logger.debug(
@ -429,45 +427,37 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
@SuppressWarnings("unchecked")
private <T extends Type> @Nullable T toAcceptedType(T originalType, Channel channel,
Function<@Nullable String, @Nullable List<Class<? extends T>>> acceptedTypesFunction, Item item) {
String acceptedItemType = channel.getAcceptedItemType();
String channelAcceptedItemType = channel.getAcceptedItemType();
// DecimalType command sent to a NumberItem with dimension defined:
if (originalType instanceof DecimalType type && hasDimension(item, acceptedItemType)) {
@Nullable
QuantityType<?> quantityType = convertToQuantityType(type, item, acceptedItemType);
if (quantityType != null) {
return (T) quantityType;
}
}
// The command is sent to an item w/o dimension defined and the channel is legacy (created from a ThingType
// definition before UoM was introduced to the binding). The dimension information might now be defined on the
// current ThingType. The binding might expect us to provide a QuantityType so try to convert to the dimension
// the ChannelType provides.
// This can be removed once a suitable solution for https://github.com/eclipse/smarthome/issues/2555 (Thing
// migration) is found.
if (originalType instanceof DecimalType type && !hasDimension(item, acceptedItemType)
&& channelTypeDefinesDimension(channel.getChannelTypeUID())) {
ChannelType channelType = channelTypeRegistry.getChannelType(channel.getChannelTypeUID());
String acceptedItemTypeFromChannelType = channelType != null ? channelType.getItemType() : null;
@Nullable
QuantityType<?> quantityType = convertToQuantityType(type, item, acceptedItemTypeFromChannelType);
if (quantityType != null) {
return (T) quantityType;
}
}
if (acceptedItemType == null) {
if (channelAcceptedItemType == null) {
return originalType;
}
List<Class<? extends T>> acceptedTypes = acceptedTypesFunction.apply(acceptedItemType);
if (acceptedTypes == null) {
return originalType;
// handle Number-Channels for backward compatibility
if (CoreItemFactory.NUMBER.equals(channelAcceptedItemType)
&& originalType instanceof QuantityType<?> quantityType) {
// strip unit from QuantityType for channels that accept plain number
return (T) new DecimalType(quantityType.toBigDecimal());
}
if (acceptedTypes.contains(originalType.getClass())) {
String itemDimension = ItemUtil.getItemTypeExtension(item.getType());
String channelDimension = ItemUtil.getItemTypeExtension(channelAcceptedItemType);
if (originalType instanceof DecimalType decimalType && channelDimension != null
&& channelDimension.equals(itemDimension)) {
// Add unit from item to DecimalType when dimensions are equal
Unit<?> unit = Objects.requireNonNull(((NumberItem) item).getUnit());
return (T) new QuantityType<>(decimalType.toBigDecimal(), unit);
}
// handle HSBType/PercentType
if (CoreItemFactory.DIMMER.equals(channelAcceptedItemType) && originalType instanceof HSBType hsb) {
return (T) (hsb.as(PercentType.class));
}
// check for other cases if the type is acceptable
List<Class<? extends T>> acceptedTypes = acceptedTypesFunction.apply(channelAcceptedItemType);
if (acceptedTypes == null || acceptedTypes.contains(originalType.getClass())) {
return originalType;
} else if (acceptedTypes.contains(PercentType.class) && originalType instanceof State state
&& PercentType.class.isAssignableFrom(originalType.getClass())) {
@ -476,77 +466,10 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
&& PercentType.class.isAssignableFrom(originalType.getClass())) {
return (@Nullable T) state.as(OnOffType.class);
} else {
// Look for class hierarchy and convert appropriately
for (Class<? extends T> typeClass : acceptedTypes) {
if (!typeClass.isEnum() && typeClass.isAssignableFrom(originalType.getClass()) //
&& State.class.isAssignableFrom(typeClass) && originalType instanceof State state) {
T ret = (T) state.as((Class<? extends State>) typeClass);
if (logger.isDebugEnabled()) {
logger.debug("Converted '{}' ({}) to accepted type '{}' ({}) for channel '{}' ", originalType,
originalType.getClass().getSimpleName(), ret, ret.getClass().getName(),
channel.getUID());
}
return ret;
}
}
}
logger.debug("Received not accepted type '{}' for channel '{}'", originalType.getClass().getSimpleName(),
channel.getUID());
return null;
}
private boolean channelTypeDefinesDimension(@Nullable ChannelTypeUID channelTypeUID) {
if (channelTypeUID == null) {
return false;
}
ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID);
return channelType != null && getDimension(channelType.getItemType()) != null;
}
private boolean hasDimension(Item item, @Nullable String acceptedItemType) {
return (item instanceof NumberItem ni && ni.getDimension() != null) || getDimension(acceptedItemType) != null;
}
private @Nullable QuantityType<?> convertToQuantityType(DecimalType originalType, Item item,
@Nullable String acceptedItemType) {
if (!(item instanceof NumberItem)) {
// PercentType command sent via DimmerItem to a channel that's dimensioned
// (such as Number:Dimensionless, expecting a %).
// We can't know the proper units to add, so just pass it through and assume
// The binding can deal with it.
logger.debug("Received not accepted type '{}' for channel '{}'", originalType.getClass().getSimpleName(),
channel.getUID());
return null;
}
NumberItem numberItem = (NumberItem) item;
// DecimalType command sent via a NumberItem with dimension:
Class<? extends Quantity<?>> dimension = numberItem.getDimension();
if (dimension == null) {
// DecimalType command sent via a plain NumberItem w/o dimension.
// We try to guess the correct unit from the channel-type's expected item dimension
// or from the item's state description.
dimension = getDimension(acceptedItemType);
}
if (dimension != null) {
return numberItem.toQuantityType(originalType, dimension);
}
return null;
}
private @Nullable Class<? extends Quantity<?>> getDimension(@Nullable String acceptedItemType) {
if (acceptedItemType == null || acceptedItemType.isEmpty()) {
return null;
}
String itemTypeExtension = ItemUtil.getItemTypeExtension(acceptedItemType);
if (itemTypeExtension == null) {
return null;
}
return UnitUtils.parseDimension(itemTypeExtension);
}
private @Nullable Item getItem(final String itemName) {
@ -556,7 +479,8 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
private void receiveTrigger(ChannelTriggeredEvent channelTriggeredEvent) {
final ChannelUID channelUID = channelTriggeredEvent.getChannel();
final String event = channelTriggeredEvent.getEvent();
final Thing thing = getThing(channelUID.getThingUID());
ThingUID thingUID = channelUID.getThingUID();
final Thing thing = thingRegistry.get(thingUID);
handleCallFromHandler(channelUID, thing, profile -> {
if (profile instanceof TriggerProfile triggerProfile) {
@ -566,7 +490,8 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
}
public void stateUpdated(ChannelUID channelUID, State state) {
final Thing thing = getThing(channelUID.getThingUID());
ThingUID thingUID = channelUID.getThingUID();
final Thing thing = thingRegistry.get(thingUID);
handleCallFromHandler(channelUID, thing, profile -> {
if (profile instanceof StateProfile stateProfile) {
@ -576,7 +501,8 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
}
public void postCommand(ChannelUID channelUID, Command command) {
final Thing thing = getThing(channelUID.getThingUID());
ThingUID thingUID = channelUID.getThingUID();
final Thing thing = thingRegistry.get(thingUID);
handleCallFromHandler(channelUID, thing, profile -> {
if (profile instanceof StateProfile stateProfile) {
@ -585,7 +511,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
});
}
void handleCallFromHandler(ChannelUID channelUID, @Nullable Thing thing, Consumer<Profile> action) {
private void handleCallFromHandler(ChannelUID channelUID, @Nullable Thing thing, Consumer<Profile> action) {
itemChannelLinkRegistry.getLinks(channelUID).forEach(link -> {
final Item item = getItem(link.getItemName());
if (item != null) {
@ -630,9 +556,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
protected void removeProfileFactory(ProfileFactory profileFactory) {
Set<String> links = profileFactories.remove(profileFactory);
synchronized (profiles) {
links.forEach(link -> {
profiles.remove(link);
});
links.forEach(profiles::remove);
}
}

View File

@ -0,0 +1,141 @@
/**
* Copyright (c) 2010-2023 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.thing;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.core.items.Item;
import org.openhab.core.library.items.CallItem;
import org.openhab.core.library.items.ColorItem;
import org.openhab.core.library.items.ContactItem;
import org.openhab.core.library.items.DateTimeItem;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.ImageItem;
import org.openhab.core.library.items.LocationItem;
import org.openhab.core.library.items.PlayerItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
/**
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class CommunicationManagerConversionTest {
// TODO: remove test - only to show CommunicationManager is too complex
private static final List<Class<? extends Item>> itemTypes = List.of(CallItem.class, ColorItem.class,
ContactItem.class, DateTimeItem.class, DimmerItem.class, ImageItem.class, LocationItem.class,
PlayerItem.class, RollershutterItem.class, StringItem.class);
private static final List<Class<? extends Type>> types = List.of(DateTimeType.class, DecimalType.class,
HSBType.class, IncreaseDecreaseType.class, NextPreviousType.class, OnOffType.class, OpenClosedType.class,
PercentType.class, PlayPauseType.class, PointType.class, QuantityType.class, RawType.class,
RewindFastforwardType.class, StringType.class, UpDownType.class, UnDefType.class);
private static Stream<Arguments> arguments()
throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
List<Arguments> arguments = new ArrayList<>();
for (Class<? extends Item> itemType : itemTypes) {
Item item = itemType.getDeclaredConstructor(String.class).newInstance("testItem");
for (Class<? extends Type> type : types) {
if (type.isEnum()) {
arguments.add(Arguments.of(item, type.getEnumConstants()[0]));
} else if (type == RawType.class) {
arguments.add(Arguments.of(item, new RawType(new byte[] {}, "mimeType")));
} else {
arguments.add(Arguments.of(item, type.getDeclaredConstructor().newInstance()));
}
}
}
return arguments.stream();
}
@Disabled
@MethodSource("arguments")
@ParameterizedTest
public void testCommand(Item item, Type originalType) {
Type returnType = null;
List<Class<? extends Command>> acceptedTypes = item.getAcceptedCommandTypes();
if (acceptedTypes.contains(originalType.getClass())) {
returnType = originalType;
} else {
// Look for class hierarchy and convert appropriately
for (Class<? extends Type> typeClass : acceptedTypes) {
if (!typeClass.isEnum() && typeClass.isAssignableFrom(originalType.getClass()) //
&& State.class.isAssignableFrom(typeClass) && originalType instanceof State state) {
returnType = state.as((Class<? extends State>) typeClass);
}
}
}
if (returnType != null && !returnType.getClass().equals(originalType.getClass())) {
fail("CommunicationManager did a conversion for target item " + item.getType() + " from "
+ originalType.getClass() + " to " + returnType.getClass());
}
}
@MethodSource("arguments")
@ParameterizedTest
public void testState(Item item, Type originalType) {
Type returnType = null;
List<Class<? extends State>> acceptedTypes = item.getAcceptedDataTypes();
if (acceptedTypes.contains(originalType.getClass())) {
returnType = originalType;
} else {
// Look for class hierarchy and convert appropriately
for (Class<? extends Type> typeClass : acceptedTypes) {
if (!typeClass.isEnum() && typeClass.isAssignableFrom(originalType.getClass()) //
&& State.class.isAssignableFrom(typeClass) && originalType instanceof State state) {
returnType = state.as((Class<? extends State>) typeClass);
}
}
}
if (returnType != null && !returnType.equals(originalType)) {
fail("CommunicationManager did a conversion for target item " + item.getType() + " from "
+ originalType.getClass() + " to " + returnType.getClass());
}
}
}

View File

@ -17,7 +17,6 @@ import javax.measure.Unit;
import javax.measure.spi.SystemOfUnits;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Provides {@link Unit}s and the current {@link SystemOfUnits}.
@ -34,9 +33,10 @@ public interface UnitProvider {
* @param dimension The {@link Quantity}, called dimension here, defines the base unit for the retrieved unit. E.g.
* call {@code getUnit(javax.measure.quantity.Temperature.class)} to retrieve the temperature unit
* according to the current {@link SystemOfUnits}.
* @return The {@link Unit} matching the given {@link Quantity}, {@code null} otherwise.
* @return The {@link Unit} matching the given {@link Quantity}
* @throws IllegalArgumentException when the dimension is unknown
*/
<T extends Quantity<T>> @Nullable Unit<T> getUnit(@Nullable Class<T> dimension);
<T extends Quantity<T>> Unit<T> getUnit(Class<T> dimension) throws IllegalArgumentException;
/**
* Returns the {@link SystemOfUnits} which is currently set, must not be null.

View File

@ -142,12 +142,12 @@ public class I18nProviderImpl
// UnitProvider
static final String MEASUREMENT_SYSTEM = "measurementSystem";
private @Nullable SystemOfUnits measurementSystem;
private final Map<Class<? extends Quantity<?>>, Map<SystemOfUnits, Unit<? extends Quantity<?>>>> dimensionMap = new HashMap<>();
private static final Map<Class<? extends Quantity<?>>, Map<SystemOfUnits, Unit<? extends Quantity<?>>>> DIMENSION_MAP = getDimensionMap();
@Activate
@SuppressWarnings("unchecked")
public I18nProviderImpl(ComponentContext componentContext) {
initDimensionMap();
getDimensionMap();
modified((Map<String, Object>) componentContext.getProperties());
this.resourceBundleTracker = new ResourceBundleTracker(componentContext.getBundleContext(), this);
@ -187,16 +187,12 @@ public class I18nProviderImpl
final SystemOfUnits newMeasurementSystem;
switch (ms) {
case SIUnits.MEASUREMENT_SYSTEM_NAME:
newMeasurementSystem = SIUnits.getInstance();
break;
case ImperialUnits.MEASUREMENT_SYSTEM_NAME:
newMeasurementSystem = ImperialUnits.getInstance();
break;
default:
case SIUnits.MEASUREMENT_SYSTEM_NAME -> newMeasurementSystem = SIUnits.getInstance();
case ImperialUnits.MEASUREMENT_SYSTEM_NAME -> newMeasurementSystem = ImperialUnits.getInstance();
default -> {
logger.debug("Error setting measurement system for value '{}'.", measurementSystem);
newMeasurementSystem = null;
break;
}
}
this.measurementSystem = newMeasurementSystem;
@ -358,12 +354,14 @@ public class I18nProviderImpl
@Override
@SuppressWarnings("unchecked")
public <T extends Quantity<T>> @Nullable Unit<T> getUnit(@Nullable Class<T> dimension) {
Map<SystemOfUnits, Unit<? extends Quantity<?>>> map = dimensionMap.get(dimension);
public <T extends Quantity<T>> Unit<T> getUnit(Class<T> dimension) {
Map<SystemOfUnits, Unit<? extends Quantity<?>>> map = DIMENSION_MAP.get(dimension);
if (map == null) {
return null;
throw new IllegalArgumentException("Dimension " + dimension.getName() + " is unknown. This is a bug.");
}
return (Unit<T>) map.get(getMeasurementSystem());
Unit<T> unit = (Unit<T>) map.get(getMeasurementSystem());
assert unit != null;
return unit;
}
@Override
@ -380,54 +378,62 @@ public class I18nProviderImpl
return SIUnits.getInstance();
}
private void initDimensionMap() {
addDefaultUnit(Acceleration.class, Units.METRE_PER_SQUARE_SECOND);
addDefaultUnit(AmountOfSubstance.class, Units.MOLE);
addDefaultUnit(Angle.class, Units.DEGREE_ANGLE, Units.DEGREE_ANGLE);
addDefaultUnit(Area.class, SIUnits.SQUARE_METRE, ImperialUnits.SQUARE_FOOT);
addDefaultUnit(ArealDensity.class, Units.DOBSON_UNIT);
addDefaultUnit(CatalyticActivity.class, Units.KATAL);
addDefaultUnit(DataAmount.class, Units.BYTE);
addDefaultUnit(DataTransferRate.class, Units.MEGABIT_PER_SECOND);
addDefaultUnit(Density.class, Units.KILOGRAM_PER_CUBICMETRE);
addDefaultUnit(Dimensionless.class, Units.ONE);
addDefaultUnit(ElectricCapacitance.class, Units.FARAD);
addDefaultUnit(ElectricCharge.class, Units.COULOMB);
addDefaultUnit(ElectricConductance.class, Units.SIEMENS);
addDefaultUnit(ElectricConductivity.class, Units.SIEMENS_PER_METRE);
addDefaultUnit(ElectricCurrent.class, Units.AMPERE);
addDefaultUnit(ElectricInductance.class, Units.HENRY);
addDefaultUnit(ElectricPotential.class, Units.VOLT);
addDefaultUnit(ElectricResistance.class, Units.OHM);
addDefaultUnit(Energy.class, Units.KILOWATT_HOUR);
addDefaultUnit(Force.class, Units.NEWTON);
addDefaultUnit(Frequency.class, Units.HERTZ);
addDefaultUnit(Illuminance.class, Units.LUX);
addDefaultUnit(Intensity.class, Units.IRRADIANCE);
addDefaultUnit(Length.class, SIUnits.METRE, ImperialUnits.INCH);
addDefaultUnit(LuminousFlux.class, Units.LUMEN);
addDefaultUnit(LuminousIntensity.class, Units.CANDELA);
addDefaultUnit(MagneticFlux.class, Units.WEBER);
addDefaultUnit(MagneticFluxDensity.class, Units.TESLA);
addDefaultUnit(Mass.class, SIUnits.KILOGRAM, ImperialUnits.POUND);
addDefaultUnit(Power.class, Units.WATT);
addDefaultUnit(Pressure.class, HECTO(SIUnits.PASCAL), ImperialUnits.INCH_OF_MERCURY);
addDefaultUnit(RadiationDoseAbsorbed.class, Units.GRAY);
addDefaultUnit(RadiationDoseEffective.class, Units.SIEVERT);
addDefaultUnit(Radioactivity.class, Units.BECQUEREL);
addDefaultUnit(SolidAngle.class, Units.STERADIAN);
addDefaultUnit(Speed.class, SIUnits.KILOMETRE_PER_HOUR, ImperialUnits.MILES_PER_HOUR);
addDefaultUnit(Temperature.class, SIUnits.CELSIUS, ImperialUnits.FAHRENHEIT);
addDefaultUnit(Time.class, Units.SECOND);
addDefaultUnit(Volume.class, SIUnits.CUBIC_METRE, ImperialUnits.GALLON_LIQUID_US);
addDefaultUnit(VolumetricFlowRate.class, Units.LITRE_PER_MINUTE, ImperialUnits.GALLON_PER_MINUTE);
public static Map<Class<? extends Quantity<?>>, Map<SystemOfUnits, Unit<? extends Quantity<?>>>> getDimensionMap() {
Map<Class<? extends Quantity<?>>, Map<SystemOfUnits, Unit<? extends Quantity<?>>>> dimensionMap = new HashMap<>();
addDefaultUnit(dimensionMap, Acceleration.class, Units.METRE_PER_SQUARE_SECOND);
addDefaultUnit(dimensionMap, AmountOfSubstance.class, Units.MOLE);
addDefaultUnit(dimensionMap, Angle.class, Units.DEGREE_ANGLE, Units.DEGREE_ANGLE);
addDefaultUnit(dimensionMap, Area.class, SIUnits.SQUARE_METRE, ImperialUnits.SQUARE_FOOT);
addDefaultUnit(dimensionMap, ArealDensity.class, Units.DOBSON_UNIT);
addDefaultUnit(dimensionMap, CatalyticActivity.class, Units.KATAL);
addDefaultUnit(dimensionMap, DataAmount.class, Units.BYTE);
addDefaultUnit(dimensionMap, DataTransferRate.class, Units.MEGABIT_PER_SECOND);
addDefaultUnit(dimensionMap, Density.class, Units.KILOGRAM_PER_CUBICMETRE);
addDefaultUnit(dimensionMap, Dimensionless.class, Units.ONE);
addDefaultUnit(dimensionMap, ElectricCapacitance.class, Units.FARAD);
addDefaultUnit(dimensionMap, ElectricCharge.class, Units.COULOMB);
addDefaultUnit(dimensionMap, ElectricConductance.class, Units.SIEMENS);
addDefaultUnit(dimensionMap, ElectricConductivity.class, Units.SIEMENS_PER_METRE);
addDefaultUnit(dimensionMap, ElectricCurrent.class, Units.AMPERE);
addDefaultUnit(dimensionMap, ElectricInductance.class, Units.HENRY);
addDefaultUnit(dimensionMap, ElectricPotential.class, Units.VOLT);
addDefaultUnit(dimensionMap, ElectricResistance.class, Units.OHM);
addDefaultUnit(dimensionMap, Energy.class, Units.KILOWATT_HOUR);
addDefaultUnit(dimensionMap, Force.class, Units.NEWTON);
addDefaultUnit(dimensionMap, Frequency.class, Units.HERTZ);
addDefaultUnit(dimensionMap, Illuminance.class, Units.LUX);
addDefaultUnit(dimensionMap, Intensity.class, Units.IRRADIANCE);
addDefaultUnit(dimensionMap, Length.class, SIUnits.METRE, ImperialUnits.INCH);
addDefaultUnit(dimensionMap, LuminousFlux.class, Units.LUMEN);
addDefaultUnit(dimensionMap, LuminousIntensity.class, Units.CANDELA);
addDefaultUnit(dimensionMap, MagneticFlux.class, Units.WEBER);
addDefaultUnit(dimensionMap, MagneticFluxDensity.class, Units.TESLA);
addDefaultUnit(dimensionMap, Mass.class, SIUnits.KILOGRAM, ImperialUnits.POUND);
addDefaultUnit(dimensionMap, Power.class, Units.WATT);
addDefaultUnit(dimensionMap, Pressure.class, HECTO(SIUnits.PASCAL), ImperialUnits.INCH_OF_MERCURY);
addDefaultUnit(dimensionMap, RadiationDoseAbsorbed.class, Units.GRAY);
addDefaultUnit(dimensionMap, RadiationDoseEffective.class, Units.SIEVERT);
addDefaultUnit(dimensionMap, Radioactivity.class, Units.BECQUEREL);
addDefaultUnit(dimensionMap, SolidAngle.class, Units.STERADIAN);
addDefaultUnit(dimensionMap, Speed.class, SIUnits.KILOMETRE_PER_HOUR, ImperialUnits.MILES_PER_HOUR);
addDefaultUnit(dimensionMap, Temperature.class, SIUnits.CELSIUS, ImperialUnits.FAHRENHEIT);
addDefaultUnit(dimensionMap, Time.class, Units.SECOND);
addDefaultUnit(dimensionMap, Volume.class, SIUnits.CUBIC_METRE, ImperialUnits.GALLON_LIQUID_US);
addDefaultUnit(dimensionMap, VolumetricFlowRate.class, Units.LITRE_PER_MINUTE, ImperialUnits.GALLON_PER_MINUTE);
return dimensionMap;
}
private <T extends Quantity<T>> void addDefaultUnit(Class<T> dimension, Unit<T> siUnit, Unit<T> imperialUnit) {
private static <T extends Quantity<T>> void addDefaultUnit(
Map<Class<? extends Quantity<?>>, Map<SystemOfUnits, Unit<? extends Quantity<?>>>> dimensionMap,
Class<T> dimension, Unit<T> siUnit, Unit<T> imperialUnit) {
dimensionMap.put(dimension, Map.of(SIUnits.getInstance(), siUnit, ImperialUnits.getInstance(), imperialUnit));
}
private <T extends Quantity<T>> void addDefaultUnit(Class<T> dimension, Unit<T> unit) {
private static <T extends Quantity<T>> void addDefaultUnit(
Map<Class<? extends Quantity<?>>, Map<SystemOfUnits, Unit<? extends Quantity<?>>>> dimensionMap,
Class<T> dimension, Unit<T> unit) {
dimensionMap.put(dimension, Map.of(SIUnits.getInstance(), unit, ImperialUnits.getInstance(), unit));
}
}

View File

@ -23,8 +23,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.AbstractRegistry;
import org.openhab.core.common.registry.Provider;
import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
@ -35,6 +35,8 @@ import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.ItemStateConverter;
import org.openhab.core.items.ItemUtil;
import org.openhab.core.items.ManagedItemProvider;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataAwareItem;
import org.openhab.core.items.MetadataRegistry;
import org.openhab.core.items.RegistryHook;
import org.openhab.core.items.events.ItemEventFactory;
@ -55,14 +57,15 @@ import org.slf4j.LoggerFactory;
* This is the main implementing class of the {@link ItemRegistry} interface. It
* keeps track of all declared items of all item providers and keeps their
* current state in memory. This is the central point where states are kept and
* thus it is a core part for all stateful services.
* thus is a core part for all stateful services.
*
* @author Kai Kreuzer - Initial contribution
* @author Stefan Bußweiler - Migration to new event mechanism
*/
@NonNullByDefault
@Component(immediate = true)
public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvider> implements ItemRegistry {
public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvider>
implements ItemRegistry, RegistryChangeListener<Metadata> {
private final Logger logger = LoggerFactory.getLogger(ItemRegistryImpl.class);
@ -70,7 +73,7 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
private @Nullable StateDescriptionService stateDescriptionService;
private @Nullable CommandDescriptionService commandDescriptionService;
private final MetadataRegistry metadataRegistry;
private @Nullable UnitProvider unitProvider;
private @Nullable ItemStateConverter itemStateConverter;
@Activate
@ -79,6 +82,19 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
this.metadataRegistry = metadataRegistry;
}
@Activate
protected void activate(final ComponentContext componentContext) {
super.activate(componentContext.getBundleContext());
metadataRegistry.addRegistryChangeListener(this);
}
@Override
@Deactivate
protected void deactivate() {
metadataRegistry.removeRegistryChangeListener(this);
super.deactivate();
}
@Override
public Item getItem(String name) throws ItemNotFoundException {
final Item item = get(name);
@ -101,13 +117,7 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
throw new ItemNotUniqueException(name, items);
}
Item item = items.iterator().next();
if (item == null) {
throw new ItemNotFoundException(name);
} else {
return item;
}
return items.iterator().next();
}
@Override
@ -146,9 +156,8 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
for (String groupName : groupItemNames) {
if (groupName != null) {
try {
Item groupItem = getItem(groupName);
if (groupItem instanceof GroupItem groupItem1) {
groupItem1.addMember(item);
if (getItem(groupName) instanceof GroupItem groupItem) {
groupItem.addMember(item);
}
} catch (ItemNotFoundException e) {
// the group might not yet be registered, let's ignore this
@ -161,9 +170,8 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
for (String groupName : groupItemNames) {
if (groupName != null) {
try {
Item groupItem = getItem(groupName);
if (groupItem instanceof GroupItem item) {
item.replaceMember(oldItem, newItem);
if (getItem(groupName) instanceof GroupItem groupItem) {
groupItem.replaceMember(oldItem, newItem);
}
} catch (ItemNotFoundException e) {
// the group might not yet be registered, let's ignore this
@ -199,9 +207,12 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
genericItem.setEventPublisher(getEventPublisher());
genericItem.setStateDescriptionService(stateDescriptionService);
genericItem.setCommandDescriptionService(commandDescriptionService);
genericItem.setUnitProvider(unitProvider);
genericItem.setItemStateConverter(itemStateConverter);
}
if (item instanceof MetadataAwareItem metadataAwareItem) {
metadataRegistry.stream().filter(m -> m.getUID().getItemName().equals(item.getName()))
.forEach(metadataAwareItem::addedMetadata);
}
}
private void addMembersToGroupItem(GroupItem groupItem) {
@ -216,9 +227,8 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
for (String groupName : groupItemNames) {
if (groupName != null) {
try {
Item groupItem = getItem(groupName);
if (groupItem instanceof GroupItem groupItem1) {
groupItem1.removeMember(item);
if (getItem(groupName) instanceof GroupItem groupItem) {
groupItem.removeMember(item);
}
} catch (ItemNotFoundException e) {
// the group might not yet be registered, let's ignore this
@ -234,16 +244,16 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
@Override
protected void onRemoveElement(Item element) {
if (element instanceof GenericItem item) {
item.dispose();
if (element instanceof GenericItem genericItem) {
genericItem.dispose();
}
removeFromGroupItems(element, element.getGroupNames());
}
@Override
protected void beforeUpdateElement(Item existingElement) {
if (existingElement instanceof GenericItem item) {
item.dispose();
if (existingElement instanceof GenericItem genericItem) {
genericItem.dispose();
}
}
@ -291,21 +301,6 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
super.unsetReadyService(readyService);
}
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
public void setUnitProvider(UnitProvider unitProvider) {
this.unitProvider = unitProvider;
for (Item item : getItems()) {
((GenericItem) item).setUnitProvider(unitProvider);
}
}
public void unsetUnitProvider(UnitProvider unitProvider) {
this.unitProvider = null;
for (Item item : getItems()) {
((GenericItem) item).setUnitProvider(null);
}
}
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
protected void setItemStateConverter(ItemStateConverter itemStateConverter) {
this.itemStateConverter = itemStateConverter;
@ -442,17 +437,6 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
registryHooks.remove(hook);
}
@Activate
protected void activate(final ComponentContext componentContext) {
super.activate(componentContext.getBundleContext());
}
@Override
@Deactivate
protected void deactivate() {
super.deactivate();
}
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
public void setStateDescriptionService(StateDescriptionService stateDescriptionService) {
this.stateDescriptionService = stateDescriptionService;
@ -495,4 +479,31 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
protected void unsetManagedProvider(ManagedItemProvider provider) {
super.unsetManagedProvider(provider);
}
@Override
public void added(Metadata element) {
String itemName = element.getUID().getItemName();
Item item = get(itemName);
if (item instanceof MetadataAwareItem metadataAwareItem) {
metadataAwareItem.addedMetadata(element);
}
}
@Override
public void removed(Metadata element) {
String itemName = element.getUID().getItemName();
Item item = get(itemName);
if (item instanceof MetadataAwareItem metadataAwareItem) {
metadataAwareItem.removedMetadata(element);
}
}
@Override
public void updated(Metadata oldElement, Metadata element) {
String itemName = element.getUID().getItemName();
Item item = get(itemName);
if (item instanceof MetadataAwareItem metadataAwareItem) {
metadataAwareItem.updatedMetadata(oldElement, element);
}
}
}

View File

@ -29,7 +29,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.service.CommandDescriptionService;
import org.openhab.core.service.StateDescriptionService;
@ -83,8 +82,6 @@ public abstract class GenericItem implements ActiveItem {
private @Nullable CommandDescriptionService commandDescriptionService;
protected @Nullable UnitProvider unitProvider;
protected @Nullable ItemStateConverter itemStateConverter;
public GenericItem(String type, String name) {
@ -176,7 +173,6 @@ public abstract class GenericItem implements ActiveItem {
this.eventPublisher = null;
this.stateDescriptionService = null;
this.commandDescriptionService = null;
this.unitProvider = null;
this.itemStateConverter = null;
}
@ -192,10 +188,6 @@ public abstract class GenericItem implements ActiveItem {
this.commandDescriptionService = commandDescriptionService;
}
public void setUnitProvider(@Nullable UnitProvider unitProvider) {
this.unitProvider = unitProvider;
}
public void setItemStateConverter(@Nullable ItemStateConverter itemStateConverter) {
this.itemStateConverter = itemStateConverter;
}

View File

@ -26,7 +26,6 @@ import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.service.CommandDescriptionService;
import org.openhab.core.service.StateDescriptionService;
@ -40,7 +39,7 @@ import org.slf4j.LoggerFactory;
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public class GroupItem extends GenericItem implements StateChangeListener {
public class GroupItem extends GenericItem implements StateChangeListener, MetadataAwareItem {
public static final String TYPE = "Group";
@ -405,14 +404,6 @@ public class GroupItem extends GenericItem implements StateChangeListener {
}
}
@Override
public void setUnitProvider(@Nullable UnitProvider unitProvider) {
super.setUnitProvider(unitProvider);
if (baseItem instanceof GenericItem item) {
item.setUnitProvider(unitProvider);
}
}
private void sendGroupStateUpdatedEvent(String memberName, State state) {
EventPublisher eventPublisher1 = this.eventPublisher;
if (eventPublisher1 != null) {
@ -457,4 +448,25 @@ public class GroupItem extends GenericItem implements StateChangeListener {
private boolean hasOwnState(GroupItem item) {
return item.getFunction() != null && item.getBaseItem() != null;
}
@Override
public void addedMetadata(Metadata metadata) {
if (baseItem instanceof MetadataAwareItem metadataAwareItem) {
metadataAwareItem.addedMetadata(metadata);
}
}
@Override
public void updatedMetadata(Metadata oldMetadata, Metadata newMetadata) {
if (baseItem instanceof MetadataAwareItem metadataAwareItem) {
metadataAwareItem.updatedMetadata(oldMetadata, newMetadata);
}
}
@Override
public void removedMetadata(Metadata metadata) {
if (baseItem instanceof MetadataAwareItem metadataAwareItem) {
metadataAwareItem.removedMetadata(metadata);
}
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2023 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.items;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link MetadataAwareItem} is an interface that can be implemented by {@link Item}s that need to be notified of
* metadata changes.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public interface MetadataAwareItem {
/**
* Can be implemented by subclasses to be informed about added metadata
*
* @param metadata the added {@link Metadata} object for this {@link Item}
*/
void addedMetadata(Metadata metadata);
/**
* Can be implemented by subclasses to be informed about updated metadata
*
* @param oldMetadata the old {@link Metadata} object for this {@link Item}
* @param newMetadata the new {@link Metadata} object for this {@link Item}
*
*/
void updatedMetadata(Metadata oldMetadata, Metadata newMetadata);
/**
* Can be implemented by subclasses to be informed about removed metadata
*
* @param metadata the removed {@link Metadata} object for this {@link Item}
*/
void removedMetadata(Metadata metadata);
}

View File

@ -14,6 +14,7 @@ package org.openhab.core.library;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.ItemFactory;
import org.openhab.core.items.ItemUtil;
@ -29,7 +30,9 @@ import org.openhab.core.library.items.PlayerItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.items.SwitchItem;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* {@link CoreItemFactory}-Implementation for the core ItemTypes
@ -54,6 +57,12 @@ public class CoreItemFactory implements ItemFactory {
public static final String ROLLERSHUTTER = "Rollershutter";
public static final String STRING = "String";
public static final String SWITCH = "Switch";
private final UnitProvider unitProvider;
@Activate
public CoreItemFactory(final @Reference UnitProvider unitProvider) {
this.unitProvider = unitProvider;
}
@Override
public @Nullable GenericItem createItem(@Nullable String itemTypeName, String itemName) {
@ -62,34 +71,21 @@ public class CoreItemFactory implements ItemFactory {
}
String itemType = ItemUtil.getMainItemType(itemTypeName);
switch (itemType) {
case CALL:
return new CallItem(itemName);
case COLOR:
return new ColorItem(itemName);
case CONTACT:
return new ContactItem(itemName);
case DATETIME:
return new DateTimeItem(itemName);
case DIMMER:
return new DimmerItem(itemName);
case IMAGE:
return new ImageItem(itemName);
case LOCATION:
return new LocationItem(itemName);
case NUMBER:
return new NumberItem(itemTypeName, itemName);
case PLAYER:
return new PlayerItem(itemName);
case ROLLERSHUTTER:
return new RollershutterItem(itemName);
case STRING:
return new StringItem(itemName);
case SWITCH:
return new SwitchItem(itemName);
default:
return null;
}
return switch (itemType) {
case CALL -> new CallItem(itemName);
case COLOR -> new ColorItem(itemName);
case CONTACT -> new ContactItem(itemName);
case DATETIME -> new DateTimeItem(itemName);
case DIMMER -> new DimmerItem(itemName);
case IMAGE -> new ImageItem(itemName);
case LOCATION -> new LocationItem(itemName);
case NUMBER -> new NumberItem(itemTypeName, itemName, unitProvider);
case PLAYER -> new PlayerItem(itemName);
case ROLLERSHUTTER -> new RollershutterItem(itemName);
case STRING -> new StringItem(itemName);
case SWITCH -> new SwitchItem(itemName);
default -> null;
};
}
@Override

View File

@ -21,11 +21,15 @@ import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.ItemUtil;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataAwareItem;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
@ -33,6 +37,8 @@ import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.UnDefType;
import org.openhab.core.types.util.UnitUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A NumberItem has a decimal value and is usually used for all kinds
@ -43,26 +49,40 @@ import org.openhab.core.types.util.UnitUtils;
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public class NumberItem extends GenericItem {
public class NumberItem extends GenericItem implements MetadataAwareItem {
public static final String UNIT_METADATA_NAMESPACE = "unit";
private static final List<Class<? extends State>> ACCEPTED_DATA_TYPES = List.of(DecimalType.class,
QuantityType.class, UnDefType.class);
private static final List<Class<? extends Command>> ACCEPTED_COMMAND_TYPES = List.of(DecimalType.class,
QuantityType.class, RefreshType.class);
@Nullable
private Class<? extends Quantity<?>> dimension;
private final Logger logger = LoggerFactory.getLogger(NumberItem.class);
private final @Nullable Class<? extends Quantity<?>> dimension;
private Unit<?> unit = Units.ONE;
private final @Nullable UnitProvider unitProvider;
public NumberItem(String name) {
this(CoreItemFactory.NUMBER, name);
this(CoreItemFactory.NUMBER, name, null);
}
public NumberItem(String type, String name) {
@SuppressWarnings({ "unchecked", "rawtypes" })
public NumberItem(String type, String name, @Nullable UnitProvider unitProvider) {
super(type, name);
this.unitProvider = unitProvider;
String itemTypeExtension = ItemUtil.getItemTypeExtension(getType());
if (itemTypeExtension != null) {
dimension = UnitUtils.parseDimension(itemTypeExtension);
if (dimension == null) {
throw new IllegalArgumentException("The given dimension " + itemTypeExtension + " is unknown.");
} else if (unitProvider == null) {
throw new IllegalArgumentException("A unit provider is required for items with a dimension.");
}
this.unit = unitProvider.getUnit((Class<? extends Quantity>) dimension);
logger.trace("Item '{}' now has unit '{}'", name, unit);
} else {
dimension = null;
}
}
@ -85,7 +105,12 @@ public class NumberItem extends GenericItem {
DecimalType strippedCommand = new DecimalType(command.toBigDecimal());
internalSend(strippedCommand);
} else {
internalSend(command);
if (command.getUnit().isCompatible(unit) || command.getUnit().inverse().isCompatible(unit)) {
internalSend(command);
} else {
logger.warn("Command '{}' to item '{}' was rejected because it is incompatible with the item unit '{}'",
command, name, unit);
}
}
}
@ -114,41 +139,34 @@ public class NumberItem extends GenericItem {
@Override
public void setState(State state) {
// QuantityType update to a NumberItem without, strip unit
if (state instanceof QuantityType quantityType && dimension == null) {
DecimalType plainState = new DecimalType(quantityType.toBigDecimal());
super.setState(plainState);
return;
}
// DecimalType update for a NumberItem with dimension, convert to QuantityType:
if (state instanceof DecimalType decimalType && dimension != null) {
Unit<?> unit = getUnit(dimension, false);
if (unit != null) {
super.setState(new QuantityType<>(decimalType.doubleValue(), unit));
return;
}
}
// QuantityType update, check unit and convert if necessary:
if (state instanceof QuantityType quantityType) {
Unit<?> itemUnit = getUnit(dimension, true);
Unit<?> stateUnit = quantityType.getUnit();
if (itemUnit != null && (!stateUnit.getSystemUnit().equals(itemUnit.getSystemUnit())
|| UnitUtils.isDifferentMeasurementSystem(itemUnit, stateUnit))) {
QuantityType<?> convertedState = quantityType.toInvertibleUnit(itemUnit);
if (state instanceof QuantityType<?> quantityType) {
if (dimension == null) {
// QuantityType update to a NumberItem without unit, strip unit
DecimalType plainState = new DecimalType(quantityType.toBigDecimal());
super.applyState(plainState);
} else {
// QuantityType update to a NumberItem with unit, convert to item unit (if possible)
Unit<?> stateUnit = quantityType.getUnit();
State convertedState = (stateUnit.isCompatible(unit) || stateUnit.inverse().isCompatible(unit))
? quantityType.toInvertibleUnit(unit)
: null;
if (convertedState != null) {
super.setState(convertedState);
return;
super.applyState(convertedState);
} else {
logger.warn("Failed to update item '{}' because '{}' could not be converted to the item unit '{}'",
name, state, unit);
}
// the state could not be converted to an accepted unit.
return;
}
}
if (isAcceptedState(ACCEPTED_DATA_TYPES, state)) {
super.setState(state);
} else if (state instanceof DecimalType decimalType) {
if (dimension == null) {
// DecimalType update to NumberItem with unit
super.applyState(decimalType);
} else {
// DecimalType update for a NumberItem with dimension, convert to QuantityType
super.applyState(new QuantityType<>(decimalType.doubleValue(), unit));
}
} else if (state instanceof UnDefType) {
super.applyState(state);
} else {
logSetTypeError(state);
}
@ -160,83 +178,54 @@ public class NumberItem extends GenericItem {
* @return the optional unit symbol for this {@link NumberItem}.
*/
public @Nullable String getUnitSymbol() {
Unit<?> unit = getUnit(dimension, true);
return unit != null ? unit.toString() : null;
return (dimension != null) ? unit.toString() : null;
}
/**
* Derive the unit for this item by the following priority:
* Get the unit for this item, either:
*
* <ul>
* <li>the unit parsed from the state description</li>
* <li>no unit if state description contains <code>%unit%</code></li>
* <li>the default system unit from the item's dimension</li>
* <li>the unit retrieved from the <code>unit</code> namespace in the item's metadata</li>
* <li>the default system unit for the item's dimension</li>
* </ul>
*
* @return the {@link Unit} for this item if available, {@code null} otherwise.
*/
public @Nullable Unit<? extends Quantity<?>> getUnit() {
return getUnit(dimension, true);
return (dimension != null) ? unit : null;
}
/**
* Try to convert a {@link DecimalType} into a new {@link QuantityType}. The unit for the new
* type is derived either from the state description (which might also give a hint on items w/o dimension) or from
* the system default unit of the given dimension.
*
* @param originalType the source {@link DecimalType}.
* @param dimension the dimension to which the new {@link QuantityType} should adhere.
* @return the new {@link QuantityType} from the given originalType, {@code null} if a unit could not be calculated.
*/
public @Nullable QuantityType<?> toQuantityType(DecimalType originalType,
@Nullable Class<? extends Quantity<?>> dimension) {
Unit<? extends Quantity<?>> itemUnit = getUnit(dimension, false);
if (itemUnit != null) {
return new QuantityType<>(originalType.toBigDecimal(), itemUnit);
}
return null;
}
/**
* Derive the unit for this item by the following priority:
* <ul>
* <li>the unit parsed from the state description</li>
* <li>the unit from the value if <code>hasUnit = true</code> and state description has unit
* <code>%unit%</code></li>
* <li>the default system unit from the (optional) dimension parameter</li>
* </ul>
*
* @param dimension the (optional) dimension
* @param hasUnit if the value has a unit
* @return the {@link Unit} for this item if available, {@code null} otherwise.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private @Nullable Unit<? extends Quantity<?>> getUnit(@Nullable Class<? extends Quantity<?>> dimension,
boolean hasUnit) {
if (dimension == null) {
// if it is a plain number without dimension, we do not have a unit.
return null;
}
StateDescription stateDescription = getStateDescription();
if (stateDescription != null) {
String pattern = stateDescription.getPattern();
if (pattern != null) {
if (hasUnit && pattern.contains(UnitUtils.UNIT_PLACEHOLDER)) {
// use provided unit if present
return null;
}
Unit<?> stateDescriptionUnit = UnitUtils.parseUnit(pattern);
if (stateDescriptionUnit != null) {
return stateDescriptionUnit;
}
@Override
public void addedMetadata(Metadata metadata) {
if (dimension != null && UNIT_METADATA_NAMESPACE.equals(metadata.getUID().getNamespace())) {
Unit<?> unit = UnitUtils.parseUnit(metadata.getValue());
if (unit == null) {
logger.warn("Unit '{}' could not be parsed to a known unit. Keeping old unit '{}' for item '{}'.",
metadata.getValue(), this.unit, name);
return;
}
if (!unit.isCompatible(this.unit) && !unit.inverse().isCompatible(this.unit)) {
logger.warn("Unit '{}' could not be parsed to a known unit. Keeping old unit '{}' for item '{}'.",
metadata.getValue(), this.unit, name);
return;
}
this.unit = unit;
logger.trace("Item '{}' now has unit '{}'", name, unit);
}
}
if (unitProvider != null) {
// explicit cast to Class<? extends Quantity> as JDK compiler complains
return unitProvider.getUnit((Class<? extends Quantity>) dimension);
@Override
public void updatedMetadata(Metadata oldMetadata, Metadata newMetadata) {
addedMetadata(newMetadata);
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void removedMetadata(Metadata metadata) {
if (dimension != null && UNIT_METADATA_NAMESPACE.equals(metadata.getUID().getNamespace())) {
assert unitProvider != null;
unit = unitProvider.getUnit((Class<? extends Quantity>) dimension);
logger.trace("Item '{}' now has unit '{}'", name, unit);
}
return null;
}
}

View File

@ -298,7 +298,9 @@ public class QuantityType<T extends Quantity<T>> extends Number
* @return the new {@link QuantityType} in the given {@link Unit} or {@code null} in case of an erro.
*/
public @Nullable QuantityType<?> toInvertibleUnit(Unit<?> targetUnit) {
if (!targetUnit.equals(getUnit()) && getUnit().inverse().isCompatible(targetUnit)) {
// only invert if unit is not equal and inverse is compatible and targetUnit is not ONE
if (!targetUnit.equals(getUnit()) && !targetUnit.isCompatible(AbstractUnit.ONE)
&& getUnit().inverse().isCompatible(targetUnit)) {
return inverse().toUnit(targetUnit);
}
return toUnit(targetUnit);

View File

@ -76,8 +76,8 @@ public class UnitUtils {
* @return the {@link Class} instance of the interface or {@code null} if the given dimension is blank.
* @throws IllegalArgumentException in case no class instance could be parsed from the given dimension.
*/
public static @Nullable Class<? extends Quantity<?>> parseDimension(String dimension) {
if (dimension.isBlank()) {
public static @Nullable Class<? extends Quantity<?>> parseDimension(@Nullable String dimension) {
if (dimension == null || dimension.isBlank()) {
return null;
}
@ -149,7 +149,7 @@ public class UnitUtils {
* label). In the latter case, the unit is expected to be the last part of the pattern separated by " " (e.g. "%.2f
* °C" for °C).
*
* @param stringWithUnit the string to extract the unit symbol from
* @param pattern the string to extract the unit symbol from
* @return the unit symbol extracted from the string or {@code null} if no unit could be parsed
*
*/
@ -173,7 +173,7 @@ public class UnitUtils {
return quantity.getUnit();
} catch (IllegalArgumentException | MeasurementParseException e) {
// we expect this exception in case the extracted string does not match any known unit
LOGGER.debug("Unknown unit from pattern: {}", unitSymbol);
LOGGER.error("Unknown unit from pattern: {}", unitSymbol);
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2023 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.internal.i18n;
import java.util.Map;
import javax.measure.Quantity;
import javax.measure.Unit;
import javax.measure.spi.SystemOfUnits;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.library.unit.SIUnits;
/**
* The {@link TestUnitProvider} implements a {@link UnitProvider} for testing purposes
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class TestUnitProvider implements UnitProvider {
private final Map<Class<? extends Quantity<?>>, Map<SystemOfUnits, Unit<? extends Quantity<?>>>> dimensionMap = I18nProviderImpl
.getDimensionMap();
@Override
@SuppressWarnings("unchecked")
public <T extends Quantity<T>> Unit<T> getUnit(Class<T> dimension) {
Unit<T> unit = (Unit<T>) dimensionMap.getOrDefault(dimension, Map.of()).get(SIUnits.getInstance());
assert unit != null;
return unit;
}
@Override
public SystemOfUnits getMeasurementSystem() {
return SIUnits.getInstance();
}
}

View File

@ -30,6 +30,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.internal.items.ExpireManager.ExpireConfig;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
@ -47,6 +48,8 @@ import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.UnDefType;
import tech.units.indriya.unit.Units;
/**
* The {@link ExpireManagerTest} tests the {@link ExpireManager}.
*
@ -341,7 +344,9 @@ class ExpireManagerTest {
// expected as state is invalid
}
testItem = new NumberItem("Number:Temperature", ITEMNAME);
UnitProvider unitProviderMock = mock(UnitProvider.class);
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(Units.CELSIUS);
testItem = new NumberItem("Number:Temperature", ITEMNAME, unitProviderMock);
cfg = new ExpireManager.ExpireConfig(testItem, "1h,15 °C", Map.of());
assertEquals(Duration.ofHours(1), cfg.duration);
assertEquals(new QuantityType<Temperature>("15 °C"), cfg.expireState);

View File

@ -26,7 +26,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.events.ItemEvent;
import org.openhab.core.items.events.ItemStateChangedEvent;
import org.openhab.core.items.events.ItemStateUpdatedEvent;
@ -160,7 +159,6 @@ public class GenericItemTest {
item.setEventPublisher(mock(EventPublisher.class));
item.setItemStateConverter(mock(ItemStateConverter.class));
item.setStateDescriptionService(null);
item.setUnitProvider(mock(UnitProvider.class));
item.addStateChangeListener(mock(StateChangeListener.class));
@ -170,7 +168,6 @@ public class GenericItemTest {
assertNull(item.itemStateConverter);
// can not be tested as stateDescriptionProviders is private in GenericItem
// assertThat(item.stateDescriptionProviders, is(nullValue()));
assertNull(item.unitProvider);
assertEquals(0, item.listeners.size());
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2023 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.items;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.library.items.NumberItem;
/**
* The {@link GroupItemTest} contains tests for {@link GroupItem}
*
* @author Jan N. Klug - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class GroupItemTest {
private static final String ITEM_NAME = "test";
private @Mock @NonNullByDefault({}) NumberItem baseItemMock;
@Test
public void testMetadataIsPropagatedToBaseItem() {
GroupItem groupItem = new GroupItem(ITEM_NAME, baseItemMock, new GroupFunction.Equality());
Metadata metadata = new Metadata(new MetadataKey("foo", ITEM_NAME), "foo", null);
Metadata updatedMetadata = new Metadata(new MetadataKey("foo", ITEM_NAME), "bar", null);
groupItem.addedMetadata(metadata);
verify(baseItemMock).addedMetadata(eq(metadata));
groupItem.updatedMetadata(metadata, updatedMetadata);
verify(baseItemMock).updatedMetadata(eq(metadata), eq(updatedMetadata));
groupItem.removedMetadata(updatedMetadata);
verify(baseItemMock).removedMetadata(eq(updatedMetadata));
}
}

View File

@ -15,6 +15,7 @@ package org.openhab.core.library;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.mockito.Mockito.when;
import java.util.List;
@ -22,18 +23,30 @@ import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.i18n.UnitProvider;
import org.openhab.core.items.GenericItem;
import org.openhab.core.library.items.NumberItem;
import tech.units.indriya.unit.Units;
/**
* @author Henning Treu - Initial contribution
*/
@NonNullByDefault
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class CoreItemFactoryTest {
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
@Test
public void shouldCreateItems() {
CoreItemFactory coreItemFactory = new CoreItemFactory();
CoreItemFactory coreItemFactory = new CoreItemFactory(unitProviderMock);
List<String> itemTypeNames = List.of(coreItemFactory.getSupportedItemTypes());
for (String itemTypeName : itemTypeNames) {
GenericItem item = coreItemFactory.createItem(itemTypeName, itemTypeName.toLowerCase());
@ -45,7 +58,8 @@ public class CoreItemFactoryTest {
@Test
public void createNumberItemWithDimension() {
CoreItemFactory coreItemFactory = new CoreItemFactory();
CoreItemFactory coreItemFactory = new CoreItemFactory(unitProviderMock);
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(Units.CELSIUS);
NumberItem numberItem = (NumberItem) coreItemFactory.createItem(CoreItemFactory.NUMBER + ":Temperature",
"myNumberItem");
@ -54,7 +68,7 @@ public class CoreItemFactoryTest {
@Test
public void shouldReturnNullForUnsupportedItemTypeName() {
CoreItemFactory coreItemFactory = new CoreItemFactory();
CoreItemFactory coreItemFactory = new CoreItemFactory(unitProviderMock);
GenericItem item = coreItemFactory.createItem("NoValidItemTypeName", "IWantMyItem");
assertThat(item, is(nullValue()));

View File

@ -14,18 +14,15 @@ package org.openhab.core.library.items;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
import java.util.List;
import java.util.Objects;
import javax.measure.quantity.Energy;
import javax.measure.Unit;
import javax.measure.quantity.Mass;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -34,16 +31,17 @@ 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.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.internal.i18n.TestUnitProvider;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataKey;
import org.openhab.core.items.events.ItemCommandEvent;
import org.openhab.core.items.events.ItemStateUpdatedEvent;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.MetricPrefix;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.service.StateDescriptionService;
@ -67,7 +65,10 @@ public class NumberItemTest {
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
private @Mock @NonNullByDefault({}) EventPublisher eventPublisherMock;
private final UnitProvider unitProvider = new TestUnitProvider();
@BeforeEach
@SuppressWarnings("unchecked")
public void setup() {
when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null))
.thenReturn(StateDescriptionFragmentBuilder.create().withPattern("%.1f " + UnitUtils.UNIT_PLACEHOLDER)
@ -75,30 +76,9 @@ public class NumberItemTest {
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
}
@Test
public void setDecimalType() {
NumberItem item = new NumberItem(ITEM_NAME);
State decimal = new DecimalType("23");
item.setState(decimal);
assertEquals(decimal, item.getState());
}
@Test
public void setPercentType() {
NumberItem item = new NumberItem(ITEM_NAME);
State percent = new PercentType(50);
item.setState(percent);
assertEquals(percent, item.getState());
}
@Test
public void setHSBType() {
NumberItem item = new NumberItem(ITEM_NAME);
State hsb = new HSBType("5,23,42");
item.setState(hsb);
assertEquals(hsb, item.getState());
}
/*
* State handling
*/
@Test
public void testUndefType() {
NumberItem item = new NumberItem(ITEM_NAME);
@ -112,46 +92,76 @@ public class NumberItemTest {
}
@Test
public void testSetQuantityTypeAccepted() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
item.setState(new QuantityType<>("20 °C"));
assertThat(item.getState(), is(new QuantityType<>("20 °C")));
}
@Test
public void testSetQuantityOnPlainNumberStripsUnit() {
public void testSetDecimalTypeToPlainItem() {
NumberItem item = new NumberItem(ITEM_NAME);
item.setState(new QuantityType<>("20 °C"));
assertThat(item.getState(), is(new DecimalType("20")));
State decimal = new DecimalType("23");
item.setState(decimal);
assertThat(item.getState(), is(decimal));
}
@Test
public void testSetQuantityTypeConverted() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
item.setState(new QuantityType<>(68, ImperialUnits.FAHRENHEIT));
assertThat(item.getState(), is(new QuantityType<>("20 °C")));
public void testSetDecimalTypeToDimensionItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
State decimal = new DecimalType("23");
item.setState(decimal);
assertThat(item.getState(), is(new QuantityType<>("23 °C")));
}
@Test
public void testSetQuantityTypeUnconverted() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
UnitProvider unitProvider = mock(UnitProvider.class);
when(unitProvider.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
item.setUnitProvider(unitProvider);
item.setState(new QuantityType<>("10 A")); // should not be accepted as valid state
public void testSetQuantityTypeToPlainItem() {
NumberItem item = new NumberItem(ITEM_NAME);
State quantity = new QuantityType<>("23 °C");
item.setState(quantity);
assertThat(item.getState(), is(new DecimalType("23")));
}
@Test
public void testSetValidQuantityTypeWithSameUnitToDimensionItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
State quantity = new QuantityType<>("23 °C");
item.setState(quantity);
assertThat(item.getState(), is(quantity));
}
@Test
public void testSetValidQuantityTypeWithDifferentUnitToDimensionItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
QuantityType<?> quantity = new QuantityType<>("23 K");
item.setState(quantity);
assertThat(item.getState(),
is(quantity.toUnit(Objects.requireNonNull(unitProvider.getUnit(Temperature.class)))));
}
@Test
public void testSetInvalidQuantityTypeToDimensionItemIsRejected() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
QuantityType<?> quantity = new QuantityType<>("23 N");
item.setState(quantity);
assertThat(item.getState(), is(UnDefType.NULL));
}
@Test
public void testCommandUnitIsPassedForDimensionItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
UnitProvider unitProvider = mock(UnitProvider.class);
when(unitProvider.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
item.setUnitProvider(unitProvider);
public void testSetPercentType() {
NumberItem item = new NumberItem(ITEM_NAME);
State percent = new PercentType(50);
item.setState(percent);
assertThat(item.getState(), is(percent));
}
@Test
public void testSetHSBType() {
NumberItem item = new NumberItem(ITEM_NAME);
State hsb = new HSBType("5,23,42");
item.setState(hsb);
assertThat(item.getState(), is(hsb));
}
/*
* Command handling
*/
@Test
public void testValidCommandUnitIsPassedForDimensionItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
EventPublisher eventPublisher = mock(EventPublisher.class);
item.setEventPublisher(eventPublisher);
@ -165,9 +175,37 @@ public class NumberItemTest {
assertThat(event.getItemCommand(), is(command));
}
@Test
public void testValidCommandDifferentUnitIsPassedForDimensionItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
EventPublisher eventPublisher = mock(EventPublisher.class);
item.setEventPublisher(eventPublisher);
QuantityType<?> command = new QuantityType<>("15 K");
item.send(command);
ArgumentCaptor<ItemCommandEvent> captor = ArgumentCaptor.forClass(ItemCommandEvent.class);
verify(eventPublisher).post(captor.capture());
ItemCommandEvent event = captor.getValue();
assertThat(event.getItemCommand(), is(command));
}
@Test
public void testInvalidCommandUnitIsRejectedForDimensionItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
EventPublisher eventPublisher = mock(EventPublisher.class);
item.setEventPublisher(eventPublisher);
QuantityType<?> command = new QuantityType<>("15 N");
item.send(command);
verify(eventPublisher, never()).post(any());
}
@Test
public void testCommandUnitIsStrippedForDimensionlessItem() {
NumberItem item = new NumberItem("Number", ITEM_NAME);
NumberItem item = new NumberItem(ITEM_NAME);
EventPublisher eventPublisher = mock(EventPublisher.class);
item.setEventPublisher(eventPublisher);
@ -180,10 +218,13 @@ public class NumberItemTest {
assertThat(event.getItemCommand(), is(new DecimalType("15")));
}
/*
* + State description handling
*/
@SuppressWarnings("null")
@Test
public void testStripUnitPlaceholderFromPlainNumberItem() {
NumberItem item = new NumberItem("Number", ITEM_NAME);
public void testStripUnitPlaceholderInStateDescriptionFromPlainNumberItem() {
NumberItem item = new NumberItem(ITEM_NAME);
item.setStateDescriptionService(stateDescriptionServiceMock);
assertThat(item.getStateDescription().getPattern(), is("%.1f"));
@ -191,20 +232,54 @@ public class NumberItemTest {
@SuppressWarnings("null")
@Test
public void testLeaveUnitPlaceholderOnDimensionNumberItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
public void testLeaveUnitPlaceholderInStateDescriptionOnDimensionNumberItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
item.setStateDescriptionService(stateDescriptionServiceMock);
assertThat(item.getStateDescription().getPattern(), is("%.1f " + UnitUtils.UNIT_PLACEHOLDER));
}
/*
* Unit / metadata handling
*/
@Test
void testSystemDefaultUnitIsUsedWithoutMetadata() {
final NumberItem item = new NumberItem("Number:Mass", ITEM_NAME, unitProvider);
assertThat(item.getUnit(), is(unitProvider.getUnit(Mass.class)));
}
@Test
void testMetadataUnitLifecycleIsObserved() {
final NumberItem item = new NumberItem("Number:Mass", ITEM_NAME, unitProvider);
Metadata initialMetadata = getUnitMetadata(MetricPrefix.MEGA(SIUnits.GRAM));
item.addedMetadata(initialMetadata);
assertThat(item.getUnit(), is(MetricPrefix.MEGA(SIUnits.GRAM)));
Metadata updatedMetadata = getUnitMetadata(MetricPrefix.MILLI(SIUnits.GRAM));
item.updatedMetadata(initialMetadata, updatedMetadata);
assertThat(item.getUnit(), is(MetricPrefix.MILLI(SIUnits.GRAM)));
item.removedMetadata(updatedMetadata);
assertThat(item.getUnit(), is(unitProvider.getUnit(Mass.class)));
}
@Test
void testInvalidMetadataUnitIsRejected() {
final NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
item.addedMetadata(getUnitMetadata(MetricPrefix.MEGA(SIUnits.GRAM)));
assertThat(item.getUnit(), is(unitProvider.getUnit(Temperature.class)));
}
/*
* Other tests
*/
@SuppressWarnings("null")
@Test
public void testMiredToKelvin() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)).thenReturn(
StateDescriptionFragmentBuilder.create().withPattern("%.0f K").build().toStateDescription());
item.setStateDescriptionService(stateDescriptionServiceMock);
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
item.addedMetadata(getUnitMetadata(Units.KELVIN));
item.setState(new QuantityType<>("370 mired"));
assertThat(item.getState().format("%.0f K"), is("2703 K"));
@ -213,130 +288,16 @@ public class NumberItemTest {
@SuppressWarnings("null")
@Test
public void testKelvinToMired() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)).thenReturn(
StateDescriptionFragmentBuilder.create().withPattern("%.0f mired").build().toStateDescription());
item.setStateDescriptionService(stateDescriptionServiceMock);
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
item.addedMetadata(getUnitMetadata(Units.MIRED));
item.setState(new QuantityType<>("2700 K"));
assertThat(item.getState().format("%.0f mired"), is("370 mired"));
}
@Test
void testStateDescriptionUnitUsedWhenStateDescriptionPresent() {
UnitProvider unitProviderMock = mock(UnitProvider.class);
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)).thenReturn(
StateDescriptionFragmentBuilder.create().withPattern("%.0f °F").build().toStateDescription());
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
item.setStateDescriptionService(stateDescriptionServiceMock);
item.setUnitProvider(unitProviderMock);
assertThat(item.getUnit(), is(ImperialUnits.FAHRENHEIT));
item.setState(new QuantityType<>("429 °F"));
assertThat(item.getState(), is(new QuantityType<>("429 °F")));
item.setState(new QuantityType<>("165 °C"));
assertThat(item.getState(), is(new QuantityType<>("329 °F")));
}
@Test
void testPreservedWhenStateDescriptionContainsWildCard() {
UnitProvider unitProviderMock = mock(UnitProvider.class);
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null))
.thenReturn(StateDescriptionFragmentBuilder.create().withPattern("%.0f " + UnitUtils.UNIT_PLACEHOLDER)
.build().toStateDescription());
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
item.setStateDescriptionService(stateDescriptionServiceMock);
item.setUnitProvider(unitProviderMock);
assertThat(item.getUnit(), is(nullValue()));
item.setState(new QuantityType<>("329 °F"));
assertThat(item.getState(), is(new QuantityType<>("329 °F")));
item.setState(new QuantityType<>("100 °C"));
assertThat(item.getState(), is(new QuantityType<>("100 °C")));
}
@Test
void testDefaultUnitUsedWhenStateDescriptionEmpty() {
UnitProvider unitProviderMock = mock(UnitProvider.class);
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
item.setUnitProvider(unitProviderMock);
assertThat(item.getUnit(), is(SIUnits.CELSIUS));
item.setState(new QuantityType<>("329 °F"));
assertThat(item.getState(), is(new QuantityType<>("165 °C")));
item.setState(new QuantityType<>("100 °C"));
assertThat(item.getState(), is(new QuantityType<>("100 °C")));
}
@Test
void testNoUnitWhenUnitPlaceholderUsed() {
final UnitProvider unitProviderMock = mock(UnitProvider.class);
when(unitProviderMock.getUnit(Energy.class)).thenReturn(Units.JOULE);
final NumberItem item = new NumberItem("Number:Energy", ITEM_NAME);
item.setUnitProvider(unitProviderMock);
assertThat(item.getUnit(), is(Units.JOULE));
item.setStateDescriptionService(stateDescriptionServiceMock);
item.setState(new QuantityType<>("329 kWh"));
assertThat(item.getState(), is(new QuantityType<>("329 kWh")));
assertThat(item.getUnit(), is(nullValue()));
}
public void quantityTypeCorrectlySetWithDifferentUnit() {
NumberItem numberItem = new NumberItem("Number:Temperature", ITEM_NAME);
numberItem.setUnitProvider(unitProviderMock);
numberItem.setEventPublisher(eventPublisherMock);
numberItem.setState(new QuantityType<>("140 °F"));
assertThat(numberItem.getState(), Matchers.is(new QuantityType<>("60 °C")));
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
verify(eventPublisherMock, times(2)).post(captor.capture());
List<Event> events = captor.getAllValues();
assertThat(events, hasSize(2));
assertThat(events.get(0), Matchers.is(instanceOf(ItemStateUpdatedEvent.class)));
ItemStateUpdatedEvent updatedEvent = (ItemStateUpdatedEvent) events.get(0);
assertThat(updatedEvent.getItemName(), Matchers.is(ITEM_NAME));
assertThat(updatedEvent.getItemState(), Matchers.is(new QuantityType<>("60°C")));
}
@Test
public void decimalTypeCorrectlySetWithUnit() {
NumberItem numberItem = new NumberItem("Number:Temperature", ITEM_NAME);
numberItem.setUnitProvider(unitProviderMock);
numberItem.setEventPublisher(eventPublisherMock);
numberItem.setState(new DecimalType(10));
assertThat(numberItem.getState(), Matchers.is(new QuantityType<>("10 °C")));
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
verify(eventPublisherMock, times(2)).post(captor.capture());
List<Event> events = captor.getAllValues();
assertThat(events, hasSize(2));
assertThat(events.get(0), Matchers.is(instanceOf(ItemStateUpdatedEvent.class)));
ItemStateUpdatedEvent updatedEvent = (ItemStateUpdatedEvent) events.get(0);
assertThat(updatedEvent.getItemName(), Matchers.is(ITEM_NAME));
assertThat(updatedEvent.getItemState(), Matchers.is(new QuantityType<>("10°C")));
private Metadata getUnitMetadata(Unit<?> unit) {
MetadataKey key = new MetadataKey(NumberItem.UNIT_METADATA_NAMESPACE, ITEM_NAME);
return new Metadata(key, unit.toString(), null);
}
}

View File

@ -32,6 +32,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.internal.i18n.TestUnitProvider;
import org.openhab.core.items.GroupFunction;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
@ -39,6 +40,7 @@ import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.osgi.service.component.ComponentContext;
/**
* @author Henning Treu - Initial contribution
@ -47,7 +49,8 @@ import org.openhab.core.types.UnDefType;
@NonNullByDefault
public class QuantityTypeArithmeticGroupFunctionTest {
private @NonNullByDefault({}) @Mock UnitProvider unitProvider;
private @Mock @NonNullByDefault({}) ComponentContext componentContext;
private final UnitProvider unitProvider = new TestUnitProvider();
/**
* Locales having a different decimal and grouping separators to test string parsing and generation.
@ -313,16 +316,14 @@ public class QuantityTypeArithmeticGroupFunctionTest {
}
private NumberItem createNumberItem(String name, Class<? extends Quantity<?>> dimension, State state) {
NumberItem item = new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name);
item.setUnitProvider(unitProvider);
NumberItem item = new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name, unitProvider);
item.setState(state);
return item;
}
private GroupItem createGroupItem(String name, Class<? extends Quantity<?>> dimension, State state) {
GroupItem item = new GroupItem(name,
new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name));
item.setUnitProvider(unitProvider);
new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name, unitProvider));
item.setState(state);
return item;
}

View File

@ -116,4 +116,9 @@ Fragment-Host: org.openhab.core.model.script
junit-platform-commons;version='[1.9.2,1.9.3)',\
junit-platform-engine;version='[1.9.2,1.9.3)',\
junit-platform-launcher;version='[1.9.2,1.9.3)',\
org.openhab.core.model.thing.runtime;version='[4.0.0,4.0.1)'
org.openhab.core.model.thing.runtime;version='[4.0.0,4.0.1)',\
net.bytebuddy.byte-buddy;version='[1.12.19,1.12.20)',\
net.bytebuddy.byte-buddy-agent;version='[1.12.19,1.12.20)',\
org.mockito.junit-jupiter;version='[4.11.0,4.11.1)',\
org.mockito.mockito-core;version='[4.11.0,4.11.1)',\
org.objenesis;version='[3.3.0,3.3.1)'

View File

@ -15,6 +15,7 @@ package org.openhab.core.model.script.engine;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
import java.util.Collection;
import java.util.List;
@ -26,8 +27,14 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.AfterEach;
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.common.registry.ProviderChangeListener;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemProvider;
import org.openhab.core.items.ItemRegistry;
@ -48,6 +55,8 @@ import org.openhab.core.types.State;
* @author Henning Treu - Initial contribution
*/
@NonNullByDefault
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class ScriptEngineOSGiTest extends JavaOSGiTest {
private static final String ITEM_NAME = "Switch1";
@ -58,9 +67,13 @@ public class ScriptEngineOSGiTest extends JavaOSGiTest {
private @NonNullByDefault({}) ItemProvider itemProvider;
private @NonNullByDefault({}) ItemRegistry itemRegistry;
private @NonNullByDefault({}) ScriptEngine scriptEngine;
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
@BeforeEach
public void setup() {
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
when(unitProviderMock.getUnit(Length.class)).thenReturn(SIUnits.METRE);
registerVolatileStorageService();
EventPublisher eventPublisher = event -> {
@ -351,7 +364,7 @@ public class ScriptEngineOSGiTest extends JavaOSGiTest {
}
private Item createNumberItem(String numberItemName, Class<?> dimension) {
return new NumberItem("Number:" + dimension.getSimpleName(), numberItemName);
return new NumberItem("Number:" + dimension.getSimpleName(), numberItemName, unitProviderMock);
}
@SuppressWarnings("unchecked")

View File

@ -17,6 +17,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
import static org.openhab.core.library.unit.Units.ONE;
import java.util.HashSet;
import java.util.LinkedList;
@ -64,6 +65,7 @@ import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.test.java.JavaOSGiTest;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
@ -796,7 +798,6 @@ public class GroupItemOSGiTest extends JavaOSGiTest {
gfDTO.name = "sum";
GroupFunction function = groupFunctionHelper.createGroupFunction(gfDTO, baseItem);
GroupItem groupItem = new GroupItem("number", baseItem, function);
groupItem.setUnitProvider(unitProviderMock);
NumberItem celsius = createNumberItem("C", Temperature.class, new QuantityType<>("23 °C"));
groupItem.addMember(celsius);
@ -820,12 +821,14 @@ public class GroupItemOSGiTest extends JavaOSGiTest {
@Test
public void assertThatNumberGroupItemWithDifferentDimensionsCalculatesCorrectState() {
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
when(unitProviderMock.getUnit(Pressure.class)).thenReturn(SIUnits.PASCAL);
when(unitProviderMock.getUnit(Dimensionless.class)).thenReturn(ONE);
NumberItem baseItem = createNumberItem("baseItem", Temperature.class, UnDefType.NULL);
GroupFunctionDTO gfDTO = new GroupFunctionDTO();
gfDTO.name = "sum";
GroupFunction function = groupFunctionHelper.createGroupFunction(gfDTO, baseItem);
GroupItem groupItem = new GroupItem("number", baseItem, function);
groupItem.setUnitProvider(unitProviderMock);
groupItem.setItemStateConverter(itemStateConverter);
NumberItem celsius = createNumberItem("C", Temperature.class, new QuantityType<>("23 °C"));
@ -844,8 +847,8 @@ public class GroupItemOSGiTest extends JavaOSGiTest {
}
private NumberItem createNumberItem(String name, Class<? extends Quantity<?>> dimension, State state) {
NumberItem item = new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name);
item.setUnitProvider(unitProviderMock);
NumberItem item = new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name,
unitProviderMock);
item.setState(state);
return item;

View File

@ -77,6 +77,7 @@ public class ItemRegistryImplTest extends JavaTest {
private @NonNullByDefault({}) ManagedItemProvider itemProvider;
private @Mock @NonNullByDefault({}) EventPublisher eventPublisherMock;
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
@BeforeEach
public void beforeEach() {
@ -92,7 +93,7 @@ public class ItemRegistryImplTest extends JavaTest {
// setup ManageItemProvider with necessary dependencies:
itemProvider = new ManagedItemProvider(new VolatileStorageService(),
new ItemBuilderFactoryImpl(new CoreItemFactory()));
new ItemBuilderFactoryImpl(new CoreItemFactory(unitProviderMock)));
itemProvider.add(new SwitchItem(ITEM_NAME));
itemProvider.add(cameraItem1);
@ -107,7 +108,6 @@ public class ItemRegistryImplTest extends JavaTest {
setManagedProvider(itemProvider);
setEventPublisher(ItemRegistryImplTest.this.eventPublisherMock);
setStateDescriptionService(mock(StateDescriptionService.class));
setUnitProvider(mock(UnitProvider.class));
setItemStateConverter(mock(ItemStateConverter.class));
}
};
@ -369,13 +369,11 @@ public class ItemRegistryImplTest extends JavaTest {
assertNotNull(item.eventPublisher);
assertNotNull(item.itemStateConverter);
assertNotNull(item.unitProvider);
itemProvider.update(new SwitchItem("Item1"));
assertNull(item.eventPublisher);
assertNull(item.itemStateConverter);
assertNull(item.unitProvider);
assertEquals(0, item.listeners.size());
}
@ -391,18 +389,6 @@ public class ItemRegistryImplTest extends JavaTest {
verify(baseItem).setStateDescriptionService(any(StateDescriptionService.class));
}
@Test
public void assertUnitProviderGetsInjected() {
GenericItem item = spy(new SwitchItem("Item1"));
NumberItem baseItem = spy(new NumberItem("baseItem"));
GenericItem group = new GroupItem("Group", baseItem);
itemProvider.add(item);
itemProvider.add(group);
verify(item).setUnitProvider(any(UnitProvider.class));
verify(baseItem).setUnitProvider(any(UnitProvider.class));
}
@Test
public void assertCommandDescriptionServiceGetsInjected() {
GenericItem item = spy(new SwitchItem("Item1"));

View File

@ -40,6 +40,8 @@ import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.ItemStateConverter;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataKey;
import org.openhab.core.items.events.ItemCommandEvent;
import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.library.CoreItemFactory;
@ -50,8 +52,8 @@ import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.service.StateDescriptionService;
import org.openhab.core.test.java.JavaOSGiTest;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@ -81,7 +83,6 @@ import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
/**
*
@ -103,21 +104,27 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
}
}
private static final UnitProvider unitProviderMock = mock(UnitProvider.class);
private static final String EVENT = "event";
private static final String ITEM_NAME_1 = "testItem1";
private static final String ITEM_NAME_2 = "testItem2";
private static final String ITEM_NAME_3 = "testItem3";
private static final String ITEM_NAME_4 = "testItem4";
private static final String ITEM_NAME_5 = "testItem5";
private static final SwitchItem ITEM_1 = new SwitchItem(ITEM_NAME_1);
private static final SwitchItem ITEM_2 = new SwitchItem(ITEM_NAME_2);
private static final NumberItem ITEM_3 = new NumberItem(ITEM_NAME_3);
private static final NumberItem ITEM_4 = new NumberItem(ITEM_NAME_4);
private static NumberItem ITEM_5 = new NumberItem(ITEM_NAME_5); // will be replaced later by dimension item
private static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID("test", "type");
private static final ThingUID THING_UID = new ThingUID("test", "thing");
private static final ChannelUID STATE_CHANNEL_UID_1 = new ChannelUID(THING_UID, "state-channel1");
private static final ChannelUID STATE_CHANNEL_UID_2 = new ChannelUID(THING_UID, "state-channel2");
private static final ChannelUID STATE_CHANNEL_UID_3 = new ChannelUID(THING_UID, "state-channel3");
private static final ChannelUID STATE_CHANNEL_UID_4 = new ChannelUID(THING_UID, "state-channel4");
private static final ChannelUID STATE_CHANNEL_UID_5 = new ChannelUID(THING_UID, "state-channel5");
private static final ChannelTypeUID CHANNEL_TYPE_UID_4 = new ChannelTypeUID("test", "channeltype");
private static final ChannelUID TRIGGER_CHANNEL_UID_1 = new ChannelUID(THING_UID, "trigger-channel1");
private static final ChannelUID TRIGGER_CHANNEL_UID_2 = new ChannelUID(THING_UID, "trigger-channel2");
@ -126,6 +133,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
private static final ItemChannelLink LINK_2_S2 = new ItemChannelLink(ITEM_NAME_2, STATE_CHANNEL_UID_2);
private static final ItemChannelLink LINK_3_S3 = new ItemChannelLink(ITEM_NAME_3, STATE_CHANNEL_UID_3);
private static final ItemChannelLink LINK_4_S4 = new ItemChannelLink(ITEM_NAME_4, STATE_CHANNEL_UID_4);
private static final ItemChannelLink LINK_5_S5 = new ItemChannelLink(ITEM_NAME_5, STATE_CHANNEL_UID_5);
private static final ItemChannelLink LINK_1_T1 = new ItemChannelLink(ITEM_NAME_1, TRIGGER_CHANNEL_UID_1);
private static final ItemChannelLink LINK_1_T2 = new ItemChannelLink(ITEM_NAME_1, TRIGGER_CHANNEL_UID_2);
private static final ItemChannelLink LINK_2_T2 = new ItemChannelLink(ITEM_NAME_2, TRIGGER_CHANNEL_UID_2);
@ -135,6 +143,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
ChannelBuilder.create(STATE_CHANNEL_UID_3, "Number:Temperature").withKind(ChannelKind.STATE).build(),
ChannelBuilder.create(STATE_CHANNEL_UID_4, CoreItemFactory.NUMBER).withKind(ChannelKind.STATE)
.withType(CHANNEL_TYPE_UID_4).build(),
ChannelBuilder.create(STATE_CHANNEL_UID_5, "Number:Temperature").withKind(ChannelKind.STATE).build(),
ChannelBuilder.create(TRIGGER_CHANNEL_UID_1).withKind(ChannelKind.TRIGGER).build(),
ChannelBuilder.create(TRIGGER_CHANNEL_UID_2).withKind(ChannelKind.TRIGGER).build()).build();
@ -158,6 +167,9 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@BeforeEach
public void beforeEach() {
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
ITEM_5 = new NumberItem("Number:Temperature", ITEM_NAME_5, unitProviderMock);
safeCaller = getService(SafeCaller.class);
assertNotNull(safeCaller);
@ -166,7 +178,8 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
assertNotNull(profileFactory);
manager = new CommunicationManager(autoUpdateManagerMock, channelTypeRegistryMock, profileFactory, iclRegistry,
itemRegistryMock, itemStateConverterMock, eventPublisherMock, safeCaller, thingRegistryMock);
itemRegistryMock, itemStateConverterMock, eventPublisherMock, safeCaller, thingRegistryMock,
unitProviderMock);
doAnswer(invocation -> {
switch (((Channel) invocation.getArguments()[0]).getKind()) {
@ -205,7 +218,8 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@Override
public Collection<ItemChannelLink> getAll() {
return List.of(LINK_1_S1, LINK_1_S2, LINK_2_S2, LINK_1_T1, LINK_1_T2, LINK_2_T2, LINK_3_S3, LINK_4_S4);
return List.of(LINK_1_S1, LINK_1_S2, LINK_2_S2, LINK_1_T1, LINK_1_T2, LINK_2_T2, LINK_3_S3, LINK_4_S4,
LINK_5_S5);
}
});
@ -213,6 +227,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
when(itemRegistryMock.get(eq(ITEM_NAME_2))).thenReturn(ITEM_2);
when(itemRegistryMock.get(eq(ITEM_NAME_3))).thenReturn(ITEM_3);
when(itemRegistryMock.get(eq(ITEM_NAME_4))).thenReturn(ITEM_4);
when(itemRegistryMock.get(eq(ITEM_NAME_5))).thenReturn(ITEM_5);
ChannelType channelType4 = mock(ChannelType.class);
when(channelType4.getItemType()).thenReturn("Number:Temperature");
@ -222,12 +237,8 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
THING.setHandler(thingHandlerMock);
when(thingRegistryMock.get(eq(THING_UID))).thenReturn(THING);
manager.addItemFactory(new CoreItemFactory());
UnitProvider unitProvider = mock(UnitProvider.class);
when(unitProvider.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
ITEM_3.setUnitProvider(unitProvider);
ITEM_4.setUnitProvider(unitProvider);
manager.addItemFactory(new CoreItemFactory(unitProviderMock));
}
@Test
@ -284,7 +295,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@Test
public void testItemCommandEventDecimal2Quantity() {
// Take unit from accepted item type (see channel built from STATE_CHANNEL_UID_3)
manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_3, DecimalType.valueOf("20")));
manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_5, DecimalType.valueOf("20")));
waitForAssert(() -> {
verify(stateProfileMock).onCommandFromItem(eq(QuantityType.valueOf("20 °C")));
});
@ -294,33 +305,16 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@Test
public void testItemCommandEventDecimal2Quantity2() {
// Take unit from state description
StateDescriptionService stateDescriptionService = mock(StateDescriptionService.class);
when(stateDescriptionService.getStateDescription(ITEM_NAME_3, null)).thenReturn(
StateDescriptionFragmentBuilder.create().withPattern("%.1f °F").build().toStateDescription());
ITEM_3.setStateDescriptionService(stateDescriptionService);
MetadataKey key = new MetadataKey(NumberItem.UNIT_METADATA_NAMESPACE, ITEM_NAME_5);
Metadata metadata = new Metadata(key, ImperialUnits.FAHRENHEIT.toString(), null);
ITEM_5.addedMetadata(metadata);
manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_3, DecimalType.valueOf("20")));
manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_5, DecimalType.valueOf("20")));
waitForAssert(() -> {
verify(stateProfileMock).onCommandFromItem(eq(QuantityType.valueOf("20 °F")));
});
verifyNoMoreInteractions(stateProfileMock);
verifyNoMoreInteractions(triggerProfileMock);
ITEM_3.setStateDescriptionService(null);
}
@Test
public void testItemCommandEventDecimal2QuantityChannelType() {
// The command is sent to an item w/o dimension defined and the channel is legacy (created from a ThingType
// definition before UoM was introduced to the binding). The dimension information might now be defined on the
// current ThingType.
manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_4, DecimalType.valueOf("20")));
waitForAssert(() -> {
verify(stateProfileMock).onCommandFromItem(eq(QuantityType.valueOf("20 °C")));
});
verifyNoMoreInteractions(stateProfileMock);
verifyNoMoreInteractions(triggerProfileMock);
}
@Test

View File

@ -25,6 +25,7 @@ import org.openhab.core.config.core.Configuration;
import org.openhab.core.items.ManagedItemProvider;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataKey;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.storage.json.internal.JsonStorage;
import org.openhab.core.thing.internal.link.ItemChannelLinkConfigDescriptionProvider;
import org.openhab.core.thing.link.ItemChannelLink;
@ -91,7 +92,7 @@ public class Upgrader {
itemStorage.getKeys().forEach(itemName -> {
ManagedItemProvider.PersistedItem item = itemStorage.get(itemName);
if (item != null && item.itemType.startsWith("Number:")) {
if (metadataStorage.containsKey("unit" + ":" + itemName)) {
if (metadataStorage.containsKey(NumberItem.UNIT_METADATA_NAMESPACE + ":" + itemName)) {
logger.debug("{}: already contains a 'unit' metadata, skipping it", itemName);
} else {
Metadata metadata = metadataStorage.get("stateDescription:" + itemName);
@ -107,7 +108,8 @@ public class Upgrader {
Unit<?> stateDescriptionUnit = UnitUtils.parseUnit(pattern);
if (stateDescriptionUnit != null) {
String unit = stateDescriptionUnit.toString();
MetadataKey defaultUnitMetadataKey = new MetadataKey("unit", itemName);
MetadataKey defaultUnitMetadataKey = new MetadataKey(NumberItem.UNIT_METADATA_NAMESPACE,
itemName);
Metadata defaultUnitMetadata = new Metadata(defaultUnitMetadataKey, unit, null);
metadataStorage.put(defaultUnitMetadataKey.toString(), defaultUnitMetadata);
logger.info("{}: Wrote 'unit={}' to metadata.", itemName, unit);