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

* Add defaultUnit metadata for NumberItem

Signed-off-by: Jan N. Klug <github@klug.nrw>
pull/3601/head
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; package org.openhab.core.automation.internal.module.handler;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.time.ZoneId; import java.time.ZoneId;
@ -20,6 +21,8 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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.automation.util.ConditionBuilder;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemRegistry;
@ -77,7 +81,9 @@ public class ItemStateConditionHandlerTest extends JavaTest {
((NumberItem) item).setState(itemState); ((NumberItem) item).setState(itemState);
break; break;
case "Number:Temperature": 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); ((NumberItem) item).setState(itemState);
break; break;
case "Dimmer": 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(23), false) }, //
{ new ParameterSet("Number", "5", new DecimalType(5), true) }, // { 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(23), false) }, //
{ new ParameterSet("Number:Temperature", "5 °C", new DecimalType(5), false) }, // { new ParameterSet("Number:Temperature", "5 °C", new DecimalType(5), true) }, //
{ new ParameterSet("Number:Temperature", "0", new QuantityType<>(), 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<>(23, SIUnits.CELSIUS), false) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, 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) }, // { 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", new DecimalType(5), false) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(23), true) }, // { new ParameterSet("Number", "5 °C", new DecimalType(23), true) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(5), false) }, // { 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<>(23, SIUnits.CELSIUS), true) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, 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), true) }, // { 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(23), true) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(5), true) }, // { new ParameterSet("Number", "5 °C", new DecimalType(5), true) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(4), false) }, // { 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<>(23, SIUnits.CELSIUS), true) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, 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) }, // { 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", new DecimalType(4), true) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(23), false) }, // { new ParameterSet("Number", "5 °C", new DecimalType(23), false) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(4), true) }, // { 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<>(23, SIUnits.CELSIUS), false) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(4, SIUnits.CELSIUS), true) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(4, SIUnits.CELSIUS), true) }, //
{ new ParameterSet("Number:Temperature", "5 °C", new QuantityType<>(23, SIUnits.CELSIUS), false) }, // { 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(23), false) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(5), true) }, // { new ParameterSet("Number", "5 °C", new DecimalType(5), true) }, //
{ new ParameterSet("Number", "5 °C", new DecimalType(4), 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<>(23, SIUnits.CELSIUS), false) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, SIUnits.CELSIUS), true) }, // { new ParameterSet("Number:Temperature", "5", new QuantityType<>(5, SIUnits.CELSIUS), true) }, //
{ new ParameterSet("Number:Temperature", "5", new QuantityType<>(4, 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); ItemStateConditionHandler handler = initItemStateConditionHandler("=", parameterSet.comparisonState);
if (parameterSet.expectedResult) { if (parameterSet.expectedResult) {
assertTrue(handler.isSatisfied(Map.of())); assertTrue(handler.isSatisfied(Map.of()),
parameterSet.item + ", comparisonState=" + parameterSet.comparisonState);
} else { } 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.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.Mockito.mock;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem; import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem; import org.openhab.core.items.GroupItem;
import org.openhab.core.library.CoreItemFactory; import org.openhab.core.library.CoreItemFactory;
@ -38,7 +40,7 @@ public class EnrichedItemDTOMapperTest extends JavaTest {
@Test @Test
public void testFiltering() { public void testFiltering() {
CoreItemFactory itemFactory = new CoreItemFactory(); CoreItemFactory itemFactory = new CoreItemFactory(mock(UnitProvider.class));
GroupItem group = new GroupItem("TestGroup"); GroupItem group = new GroupItem("TestGroup");
GroupItem subGroup = new GroupItem("TestSubGroup"); 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.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; 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.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem; import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem; import org.openhab.core.items.GroupItem;
import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemNotFoundException;
@ -43,20 +45,22 @@ import org.openhab.core.semantics.model.location.Indoor;
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class SemanticsTest { public class SemanticsTest {
private @Mock ItemRegistry mockedItemRegistry; private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock;
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
private GroupItem indoorLocationItem; private @NonNullByDefault({}) GroupItem indoorLocationItem;
private GroupItem bathroomLocationItem; private @NonNullByDefault({}) GroupItem bathroomLocationItem;
private GroupItem equipmentItem; private @NonNullByDefault({}) GroupItem equipmentItem;
private GenericItem temperaturePointItem; private @NonNullByDefault({}) GenericItem temperaturePointItem;
private GenericItem humidityPointItem; private @NonNullByDefault({}) GenericItem humidityPointItem;
private GenericItem subEquipmentItem; private @NonNullByDefault({}) GenericItem subEquipmentItem;
@BeforeEach @BeforeEach
public void setup() throws ItemNotFoundException { public void setup() throws ItemNotFoundException {
CoreItemFactory itemFactory = new CoreItemFactory(); CoreItemFactory itemFactory = new CoreItemFactory(unitProviderMock);
indoorLocationItem = new GroupItem("TestHouse"); indoorLocationItem = new GroupItem("TestHouse");
indoorLocationItem.addTag("Indoor"); indoorLocationItem.addTag("Indoor");
@ -94,13 +98,13 @@ public class SemanticsTest {
equipmentItem.addMember(subEquipmentItem); equipmentItem.addMember(subEquipmentItem);
subEquipmentItem.addGroupName(equipmentItem.getName()); subEquipmentItem.addGroupName(equipmentItem.getName());
when(mockedItemRegistry.getItem("TestHouse")).thenReturn(indoorLocationItem); when(itemRegistryMock.getItem("TestHouse")).thenReturn(indoorLocationItem);
when(mockedItemRegistry.getItem("TestBathRoom")).thenReturn(bathroomLocationItem); when(itemRegistryMock.getItem("TestBathRoom")).thenReturn(bathroomLocationItem);
when(mockedItemRegistry.getItem("Test08")).thenReturn(equipmentItem); when(itemRegistryMock.getItem("Test08")).thenReturn(equipmentItem);
when(mockedItemRegistry.getItem("TestTemperature")).thenReturn(temperaturePointItem); when(itemRegistryMock.getItem("TestTemperature")).thenReturn(temperaturePointItem);
when(mockedItemRegistry.getItem("TestHumidity")).thenReturn(humidityPointItem); when(itemRegistryMock.getItem("TestHumidity")).thenReturn(humidityPointItem);
new SemanticsActionService(mockedItemRegistry); new SemanticsActionService(itemRegistryMock);
} }
@Test @Test

View File

@ -82,11 +82,10 @@ public class PersistenceExtensionsTest {
public void setUp() { public void setUp() {
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
CoreItemFactory itemFactory = new CoreItemFactory(); CoreItemFactory itemFactory = new CoreItemFactory(unitProviderMock);
numberItem = itemFactory.createItem(CoreItemFactory.NUMBER, TEST_NUMBER); numberItem = itemFactory.createItem(CoreItemFactory.NUMBER, TEST_NUMBER);
quantityItem = itemFactory.createItem(CoreItemFactory.NUMBER + ItemUtil.EXTENSION_SEPARATOR + "Temperature", quantityItem = itemFactory.createItem(CoreItemFactory.NUMBER + ItemUtil.EXTENSION_SEPARATOR + "Temperature",
TEST_QUANTITY_NUMBER); TEST_QUANTITY_NUMBER);
quantityItem.setUnitProvider(unitProviderMock);
switchItem = itemFactory.createItem(CoreItemFactory.SWITCH, TEST_SWITCH); switchItem = itemFactory.createItem(CoreItemFactory.SWITCH, TEST_SWITCH);
when(itemRegistryMock.get(TEST_NUMBER)).thenReturn(numberItem); 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.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import java.util.Locale; import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem; import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem; import org.openhab.core.items.GroupItem;
import org.openhab.core.library.CoreItemFactory; import org.openhab.core.library.CoreItemFactory;
@ -49,7 +51,7 @@ public class SemanticTagsTest {
@BeforeEach @BeforeEach
public void setup() { public void setup() {
CoreItemFactory itemFactory = new CoreItemFactory(); CoreItemFactory itemFactory = new CoreItemFactory(mock(UnitProvider.class));
locationItem = new GroupItem("TestBathRoom"); locationItem = new GroupItem("TestBathRoom");
locationItem.addTag("Bathroom"); locationItem.addTag("Bathroom");

View File

@ -13,10 +13,12 @@
package org.openhab.core.semantics; package org.openhab.core.semantics;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem; import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem; import org.openhab.core.items.GroupItem;
import org.openhab.core.library.CoreItemFactory; 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; 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 * @author Christoph Weitkamp - Initial contribution
*/ */
@ -37,7 +39,7 @@ public class SemanticsPredicatesTest {
@BeforeEach @BeforeEach
public void setup() { public void setup() {
CoreItemFactory itemFactory = new CoreItemFactory(); CoreItemFactory itemFactory = new CoreItemFactory(mock(UnitProvider.class));
locationItem = new GroupItem("TestBathRoom"); locationItem = new GroupItem("TestBathRoom");
locationItem.addTag("Bathroom"); locationItem.addTag("Bathroom");

View File

@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem; import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem; import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
@ -43,6 +44,7 @@ public class SemanticsServiceImplTest {
private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock; private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock;
private @Mock @NonNullByDefault({}) MetadataRegistry metadataRegistryMock; private @Mock @NonNullByDefault({}) MetadataRegistry metadataRegistryMock;
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
private @NonNullByDefault({}) GroupItem locationItem; private @NonNullByDefault({}) GroupItem locationItem;
private @NonNullByDefault({}) GroupItem equipmentItem; private @NonNullByDefault({}) GroupItem equipmentItem;
@ -52,7 +54,7 @@ public class SemanticsServiceImplTest {
@BeforeEach @BeforeEach
public void setup() throws Exception { public void setup() throws Exception {
CoreItemFactory itemFactory = new CoreItemFactory(); CoreItemFactory itemFactory = new CoreItemFactory(unitProviderMock);
locationItem = new GroupItem("TestBathRoom"); locationItem = new GroupItem("TestBathRoom");
locationItem.addTag("Bathroom"); locationItem.addTag("Bathroom");
locationItem.setLabel("Joe's Room"); 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.Consumer;
import java.util.function.Function; import java.util.function.Function;
import javax.measure.Quantity; import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.Event;
import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventPublisher;
import org.openhab.core.events.EventSubscriber; import org.openhab.core.events.EventSubscriber;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.items.ItemFactory; import org.openhab.core.items.ItemFactory;
import org.openhab.core.items.ItemRegistry; 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.GroupStateUpdatedEvent;
import org.openhab.core.items.events.ItemCommandEvent; import org.openhab.core.items.events.ItemCommandEvent;
import org.openhab.core.items.events.ItemStateUpdatedEvent; 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.items.NumberItem;
import org.openhab.core.library.types.DecimalType; 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.OnOffType;
import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType; 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.ProfileTypeUID;
import org.openhab.core.thing.profiles.StateProfile; import org.openhab.core.thing.profiles.StateProfile;
import org.openhab.core.thing.profiles.TriggerProfile; 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.ChannelTypeRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.Type; 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.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Deactivate;
@ -131,6 +131,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
private final EventPublisher eventPublisher; private final EventPublisher eventPublisher;
private final SafeCaller safeCaller; private final SafeCaller safeCaller;
private final ThingRegistry thingRegistry; private final ThingRegistry thingRegistry;
private final UnitProvider unitProvider;
private final ExpiringCacheMap<Integer, Profile> profileSafeCallCache = new ExpiringCacheMap<>(CACHE_EXPIRATION); 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 ItemStateConverter itemStateConverter, //
final @Reference EventPublisher eventPublisher, // final @Reference EventPublisher eventPublisher, //
final @Reference SafeCaller safeCaller, // final @Reference SafeCaller safeCaller, //
final @Reference ThingRegistry thingRegistry) { final @Reference ThingRegistry thingRegistry, final @Reference UnitProvider unitProvider) {
this.autoUpdateManager = autoUpdateManager; this.autoUpdateManager = autoUpdateManager;
this.channelTypeRegistry = channelTypeRegistry; this.channelTypeRegistry = channelTypeRegistry;
this.defaultProfileFactory = defaultProfileFactory; this.defaultProfileFactory = defaultProfileFactory;
@ -153,6 +154,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
this.eventPublisher = eventPublisher; this.eventPublisher = eventPublisher;
this.safeCaller = safeCaller; this.safeCaller = safeCaller;
this.thingRegistry = thingRegistry; this.thingRegistry = thingRegistry;
this.unitProvider = unitProvider;
itemChannelLinkRegistry.addRegistryChangeListener(this); 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) { private Profile getProfile(ItemChannelLink link, Item item, @Nullable Thing thing) {
synchronized (profiles) { synchronized (profiles) {
Profile profile = profiles.get(link.getUID()); Profile profile = profiles.get(link.getUID());
@ -228,8 +226,8 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
} }
private ProfileCallback createCallback(ItemChannelLink link) { private ProfileCallback createCallback(ItemChannelLink link) {
return new ProfileCallbackImpl(eventPublisher, safeCaller, itemStateConverter, link, return new ProfileCallbackImpl(eventPublisher, safeCaller, itemStateConverter, link, thingRegistry::get,
thingUID -> getThing(thingUID), itemName -> getItem(itemName)); this::getItem);
} }
private @Nullable ProfileTypeUID determineProfileTypeUID(ItemChannelLink link, Item item, @Nullable Thing thing) { 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() String profileName = (String) link.getConfiguration()
.get(ItemChannelLinkConfigDescriptionProvider.PARAM_PROFILE); .get(ItemChannelLinkConfigDescriptionProvider.PARAM_PROFILE);
if (profileName != null && !profileName.trim().isEmpty()) { 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 new ProfileTypeUID(profileName);
} }
return null; 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, private @Nullable Profile getProfileFromFactories(ProfileTypeUID profileTypeUID, ItemChannelLink link,
ProfileCallback callback) { ProfileCallback callback) {
ProfileContext context = null; ProfileContext context = null;
Item item = getItem(link.getItemName()); 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) { if (item != null && thing != null) {
Channel channel = thing.getChannel(link.getLinkedUID()); Channel channel = thing.getChannel(link.getLinkedUID());
if (channel != null) { if (channel != null) {
@ -341,48 +335,51 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
autoUpdateManager.receiveCommand(commandEvent, item); autoUpdateManager.receiveCommand(commandEvent, item);
} }
handleEvent(itemName, command, commandEvent.getSource(), s -> acceptedCommandTypeMap.get(s), handleEvent(itemName, command, commandEvent.getSource(), acceptedCommandTypeMap::get,
(profile, thing, convertedCommand) -> { this::applyProfileForCommand);
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 void receiveUpdate(ItemStateUpdatedEvent updateEvent) { private void receiveUpdate(ItemStateUpdatedEvent updateEvent) {
final String itemName = updateEvent.getItemName(); final String itemName = updateEvent.getItemName();
final State newState = updateEvent.getItemState(); final State newState = updateEvent.getItemState();
handleEvent(itemName, newState, updateEvent.getSource(), s -> acceptedStateTypeMap.get(s), handleEvent(itemName, newState, updateEvent.getSource(), acceptedStateTypeMap::get,
(profile, thing, convertedState) -> { this::applyProfileForUpdate);
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!");
}
});
} }
@FunctionalInterface @FunctionalInterface
private static interface ProfileAction<T extends Type> { private interface ProfileAction<T extends Type> {
void handle(Profile profile, Thing thing, T 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, 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); return !link.getLinkedUID().toString().equals(source);
}).forEach(link -> { }).forEach(link -> {
ChannelUID channelUID = link.getLinkedUID(); ChannelUID channelUID = link.getLinkedUID();
Thing thing = getThing(channelUID.getThingUID()); ThingUID thingUID = channelUID.getThingUID();
Thing thing = thingRegistry.get(thingUID);
if (thing != null) { if (thing != null) {
Channel channel = thing.getChannel(channelUID.getId()); Channel channel = thing.getChannel(channelUID.getId());
if (channel != null) { if (channel != null) {
@ -408,7 +406,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
if (convertedType != null) { if (convertedType != null) {
if (thing.getHandler() != null) { if (thing.getHandler() != null) {
Profile profile = getProfile(link, item, thing); Profile profile = getProfile(link, item, thing);
action.handle(profile, thing, convertedType); action.applyProfile(profile, thing, convertedType);
} }
} else { } else {
logger.debug( logger.debug(
@ -429,45 +427,37 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T extends Type> @Nullable T toAcceptedType(T originalType, Channel channel, private <T extends Type> @Nullable T toAcceptedType(T originalType, Channel channel,
Function<@Nullable String, @Nullable List<Class<? extends T>>> acceptedTypesFunction, Item item) { 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 (channelAcceptedItemType == null) {
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) {
return originalType; return originalType;
} }
List<Class<? extends T>> acceptedTypes = acceptedTypesFunction.apply(acceptedItemType); // handle Number-Channels for backward compatibility
if (acceptedTypes == null) { if (CoreItemFactory.NUMBER.equals(channelAcceptedItemType)
return originalType; && 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; return originalType;
} else if (acceptedTypes.contains(PercentType.class) && originalType instanceof State state } else if (acceptedTypes.contains(PercentType.class) && originalType instanceof State state
&& PercentType.class.isAssignableFrom(originalType.getClass())) { && PercentType.class.isAssignableFrom(originalType.getClass())) {
@ -476,77 +466,10 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
&& PercentType.class.isAssignableFrom(originalType.getClass())) { && PercentType.class.isAssignableFrom(originalType.getClass())) {
return (@Nullable T) state.as(OnOffType.class); return (@Nullable T) state.as(OnOffType.class);
} else { } else {
// Look for class hierarchy and convert appropriately logger.debug("Received not accepted type '{}' for channel '{}'", originalType.getClass().getSimpleName(),
for (Class<? extends T> typeClass : acceptedTypes) { channel.getUID());
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.
return null; 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) { private @Nullable Item getItem(final String itemName) {
@ -556,7 +479,8 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
private void receiveTrigger(ChannelTriggeredEvent channelTriggeredEvent) { private void receiveTrigger(ChannelTriggeredEvent channelTriggeredEvent) {
final ChannelUID channelUID = channelTriggeredEvent.getChannel(); final ChannelUID channelUID = channelTriggeredEvent.getChannel();
final String event = channelTriggeredEvent.getEvent(); 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 -> { handleCallFromHandler(channelUID, thing, profile -> {
if (profile instanceof TriggerProfile triggerProfile) { if (profile instanceof TriggerProfile triggerProfile) {
@ -566,7 +490,8 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
} }
public void stateUpdated(ChannelUID channelUID, State state) { 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 -> { handleCallFromHandler(channelUID, thing, profile -> {
if (profile instanceof StateProfile stateProfile) { if (profile instanceof StateProfile stateProfile) {
@ -576,7 +501,8 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
} }
public void postCommand(ChannelUID channelUID, Command command) { 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 -> { handleCallFromHandler(channelUID, thing, profile -> {
if (profile instanceof StateProfile stateProfile) { 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 -> { itemChannelLinkRegistry.getLinks(channelUID).forEach(link -> {
final Item item = getItem(link.getItemName()); final Item item = getItem(link.getItemName());
if (item != null) { if (item != null) {
@ -630,9 +556,7 @@ public class CommunicationManager implements EventSubscriber, RegistryChangeList
protected void removeProfileFactory(ProfileFactory profileFactory) { protected void removeProfileFactory(ProfileFactory profileFactory) {
Set<String> links = profileFactories.remove(profileFactory); Set<String> links = profileFactories.remove(profileFactory);
synchronized (profiles) { synchronized (profiles) {
links.forEach(link -> { links.forEach(profiles::remove);
profiles.remove(link);
});
} }
} }

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 javax.measure.spi.SystemOfUnits;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/** /**
* Provides {@link Unit}s and the current {@link SystemOfUnits}. * 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. * @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 * call {@code getUnit(javax.measure.quantity.Temperature.class)} to retrieve the temperature unit
* according to the current {@link SystemOfUnits}. * 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. * Returns the {@link SystemOfUnits} which is currently set, must not be null.

View File

@ -142,12 +142,12 @@ public class I18nProviderImpl
// UnitProvider // UnitProvider
static final String MEASUREMENT_SYSTEM = "measurementSystem"; static final String MEASUREMENT_SYSTEM = "measurementSystem";
private @Nullable SystemOfUnits 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 @Activate
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public I18nProviderImpl(ComponentContext componentContext) { public I18nProviderImpl(ComponentContext componentContext) {
initDimensionMap(); getDimensionMap();
modified((Map<String, Object>) componentContext.getProperties()); modified((Map<String, Object>) componentContext.getProperties());
this.resourceBundleTracker = new ResourceBundleTracker(componentContext.getBundleContext(), this); this.resourceBundleTracker = new ResourceBundleTracker(componentContext.getBundleContext(), this);
@ -187,16 +187,12 @@ public class I18nProviderImpl
final SystemOfUnits newMeasurementSystem; final SystemOfUnits newMeasurementSystem;
switch (ms) { switch (ms) {
case SIUnits.MEASUREMENT_SYSTEM_NAME: case SIUnits.MEASUREMENT_SYSTEM_NAME -> newMeasurementSystem = SIUnits.getInstance();
newMeasurementSystem = SIUnits.getInstance(); case ImperialUnits.MEASUREMENT_SYSTEM_NAME -> newMeasurementSystem = ImperialUnits.getInstance();
break; default -> {
case ImperialUnits.MEASUREMENT_SYSTEM_NAME:
newMeasurementSystem = ImperialUnits.getInstance();
break;
default:
logger.debug("Error setting measurement system for value '{}'.", measurementSystem); logger.debug("Error setting measurement system for value '{}'.", measurementSystem);
newMeasurementSystem = null; newMeasurementSystem = null;
break; }
} }
this.measurementSystem = newMeasurementSystem; this.measurementSystem = newMeasurementSystem;
@ -358,12 +354,14 @@ public class I18nProviderImpl
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T extends Quantity<T>> @Nullable Unit<T> getUnit(@Nullable Class<T> dimension) { public <T extends Quantity<T>> Unit<T> getUnit(Class<T> dimension) {
Map<SystemOfUnits, Unit<? extends Quantity<?>>> map = dimensionMap.get(dimension); Map<SystemOfUnits, Unit<? extends Quantity<?>>> map = DIMENSION_MAP.get(dimension);
if (map == null) { 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 @Override
@ -380,54 +378,62 @@ public class I18nProviderImpl
return SIUnits.getInstance(); return SIUnits.getInstance();
} }
private void initDimensionMap() { public static Map<Class<? extends Quantity<?>>, Map<SystemOfUnits, Unit<? extends Quantity<?>>>> getDimensionMap() {
addDefaultUnit(Acceleration.class, Units.METRE_PER_SQUARE_SECOND); Map<Class<? extends Quantity<?>>, Map<SystemOfUnits, Unit<? extends Quantity<?>>>> dimensionMap = new HashMap<>();
addDefaultUnit(AmountOfSubstance.class, Units.MOLE);
addDefaultUnit(Angle.class, Units.DEGREE_ANGLE, Units.DEGREE_ANGLE); addDefaultUnit(dimensionMap, Acceleration.class, Units.METRE_PER_SQUARE_SECOND);
addDefaultUnit(Area.class, SIUnits.SQUARE_METRE, ImperialUnits.SQUARE_FOOT); addDefaultUnit(dimensionMap, AmountOfSubstance.class, Units.MOLE);
addDefaultUnit(ArealDensity.class, Units.DOBSON_UNIT); addDefaultUnit(dimensionMap, Angle.class, Units.DEGREE_ANGLE, Units.DEGREE_ANGLE);
addDefaultUnit(CatalyticActivity.class, Units.KATAL); addDefaultUnit(dimensionMap, Area.class, SIUnits.SQUARE_METRE, ImperialUnits.SQUARE_FOOT);
addDefaultUnit(DataAmount.class, Units.BYTE); addDefaultUnit(dimensionMap, ArealDensity.class, Units.DOBSON_UNIT);
addDefaultUnit(DataTransferRate.class, Units.MEGABIT_PER_SECOND); addDefaultUnit(dimensionMap, CatalyticActivity.class, Units.KATAL);
addDefaultUnit(Density.class, Units.KILOGRAM_PER_CUBICMETRE); addDefaultUnit(dimensionMap, DataAmount.class, Units.BYTE);
addDefaultUnit(Dimensionless.class, Units.ONE); addDefaultUnit(dimensionMap, DataTransferRate.class, Units.MEGABIT_PER_SECOND);
addDefaultUnit(ElectricCapacitance.class, Units.FARAD); addDefaultUnit(dimensionMap, Density.class, Units.KILOGRAM_PER_CUBICMETRE);
addDefaultUnit(ElectricCharge.class, Units.COULOMB); addDefaultUnit(dimensionMap, Dimensionless.class, Units.ONE);
addDefaultUnit(ElectricConductance.class, Units.SIEMENS); addDefaultUnit(dimensionMap, ElectricCapacitance.class, Units.FARAD);
addDefaultUnit(ElectricConductivity.class, Units.SIEMENS_PER_METRE); addDefaultUnit(dimensionMap, ElectricCharge.class, Units.COULOMB);
addDefaultUnit(ElectricCurrent.class, Units.AMPERE); addDefaultUnit(dimensionMap, ElectricConductance.class, Units.SIEMENS);
addDefaultUnit(ElectricInductance.class, Units.HENRY); addDefaultUnit(dimensionMap, ElectricConductivity.class, Units.SIEMENS_PER_METRE);
addDefaultUnit(ElectricPotential.class, Units.VOLT); addDefaultUnit(dimensionMap, ElectricCurrent.class, Units.AMPERE);
addDefaultUnit(ElectricResistance.class, Units.OHM); addDefaultUnit(dimensionMap, ElectricInductance.class, Units.HENRY);
addDefaultUnit(Energy.class, Units.KILOWATT_HOUR); addDefaultUnit(dimensionMap, ElectricPotential.class, Units.VOLT);
addDefaultUnit(Force.class, Units.NEWTON); addDefaultUnit(dimensionMap, ElectricResistance.class, Units.OHM);
addDefaultUnit(Frequency.class, Units.HERTZ); addDefaultUnit(dimensionMap, Energy.class, Units.KILOWATT_HOUR);
addDefaultUnit(Illuminance.class, Units.LUX); addDefaultUnit(dimensionMap, Force.class, Units.NEWTON);
addDefaultUnit(Intensity.class, Units.IRRADIANCE); addDefaultUnit(dimensionMap, Frequency.class, Units.HERTZ);
addDefaultUnit(Length.class, SIUnits.METRE, ImperialUnits.INCH); addDefaultUnit(dimensionMap, Illuminance.class, Units.LUX);
addDefaultUnit(LuminousFlux.class, Units.LUMEN); addDefaultUnit(dimensionMap, Intensity.class, Units.IRRADIANCE);
addDefaultUnit(LuminousIntensity.class, Units.CANDELA); addDefaultUnit(dimensionMap, Length.class, SIUnits.METRE, ImperialUnits.INCH);
addDefaultUnit(MagneticFlux.class, Units.WEBER); addDefaultUnit(dimensionMap, LuminousFlux.class, Units.LUMEN);
addDefaultUnit(MagneticFluxDensity.class, Units.TESLA); addDefaultUnit(dimensionMap, LuminousIntensity.class, Units.CANDELA);
addDefaultUnit(Mass.class, SIUnits.KILOGRAM, ImperialUnits.POUND); addDefaultUnit(dimensionMap, MagneticFlux.class, Units.WEBER);
addDefaultUnit(Power.class, Units.WATT); addDefaultUnit(dimensionMap, MagneticFluxDensity.class, Units.TESLA);
addDefaultUnit(Pressure.class, HECTO(SIUnits.PASCAL), ImperialUnits.INCH_OF_MERCURY); addDefaultUnit(dimensionMap, Mass.class, SIUnits.KILOGRAM, ImperialUnits.POUND);
addDefaultUnit(RadiationDoseAbsorbed.class, Units.GRAY); addDefaultUnit(dimensionMap, Power.class, Units.WATT);
addDefaultUnit(RadiationDoseEffective.class, Units.SIEVERT); addDefaultUnit(dimensionMap, Pressure.class, HECTO(SIUnits.PASCAL), ImperialUnits.INCH_OF_MERCURY);
addDefaultUnit(Radioactivity.class, Units.BECQUEREL); addDefaultUnit(dimensionMap, RadiationDoseAbsorbed.class, Units.GRAY);
addDefaultUnit(SolidAngle.class, Units.STERADIAN); addDefaultUnit(dimensionMap, RadiationDoseEffective.class, Units.SIEVERT);
addDefaultUnit(Speed.class, SIUnits.KILOMETRE_PER_HOUR, ImperialUnits.MILES_PER_HOUR); addDefaultUnit(dimensionMap, Radioactivity.class, Units.BECQUEREL);
addDefaultUnit(Temperature.class, SIUnits.CELSIUS, ImperialUnits.FAHRENHEIT); addDefaultUnit(dimensionMap, SolidAngle.class, Units.STERADIAN);
addDefaultUnit(Time.class, Units.SECOND); addDefaultUnit(dimensionMap, Speed.class, SIUnits.KILOMETRE_PER_HOUR, ImperialUnits.MILES_PER_HOUR);
addDefaultUnit(Volume.class, SIUnits.CUBIC_METRE, ImperialUnits.GALLON_LIQUID_US); addDefaultUnit(dimensionMap, Temperature.class, SIUnits.CELSIUS, ImperialUnits.FAHRENHEIT);
addDefaultUnit(VolumetricFlowRate.class, Units.LITRE_PER_MINUTE, ImperialUnits.GALLON_PER_MINUTE); 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)); 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)); 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.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.AbstractRegistry; import org.openhab.core.common.registry.AbstractRegistry;
import org.openhab.core.common.registry.Provider; import org.openhab.core.common.registry.Provider;
import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem; import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem; import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item; 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.ItemStateConverter;
import org.openhab.core.items.ItemUtil; import org.openhab.core.items.ItemUtil;
import org.openhab.core.items.ManagedItemProvider; 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.MetadataRegistry;
import org.openhab.core.items.RegistryHook; import org.openhab.core.items.RegistryHook;
import org.openhab.core.items.events.ItemEventFactory; 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 * 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 * 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 * 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 Kai Kreuzer - Initial contribution
* @author Stefan Bußweiler - Migration to new event mechanism * @author Stefan Bußweiler - Migration to new event mechanism
*/ */
@NonNullByDefault @NonNullByDefault
@Component(immediate = true) @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); 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 StateDescriptionService stateDescriptionService;
private @Nullable CommandDescriptionService commandDescriptionService; private @Nullable CommandDescriptionService commandDescriptionService;
private final MetadataRegistry metadataRegistry; private final MetadataRegistry metadataRegistry;
private @Nullable UnitProvider unitProvider;
private @Nullable ItemStateConverter itemStateConverter; private @Nullable ItemStateConverter itemStateConverter;
@Activate @Activate
@ -79,6 +82,19 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
this.metadataRegistry = metadataRegistry; 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 @Override
public Item getItem(String name) throws ItemNotFoundException { public Item getItem(String name) throws ItemNotFoundException {
final Item item = get(name); final Item item = get(name);
@ -101,13 +117,7 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
throw new ItemNotUniqueException(name, items); throw new ItemNotUniqueException(name, items);
} }
Item item = items.iterator().next(); return items.iterator().next();
if (item == null) {
throw new ItemNotFoundException(name);
} else {
return item;
}
} }
@Override @Override
@ -146,9 +156,8 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
for (String groupName : groupItemNames) { for (String groupName : groupItemNames) {
if (groupName != null) { if (groupName != null) {
try { try {
Item groupItem = getItem(groupName); if (getItem(groupName) instanceof GroupItem groupItem) {
if (groupItem instanceof GroupItem groupItem1) { groupItem.addMember(item);
groupItem1.addMember(item);
} }
} catch (ItemNotFoundException e) { } catch (ItemNotFoundException e) {
// the group might not yet be registered, let's ignore this // 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) { for (String groupName : groupItemNames) {
if (groupName != null) { if (groupName != null) {
try { try {
Item groupItem = getItem(groupName); if (getItem(groupName) instanceof GroupItem groupItem) {
if (groupItem instanceof GroupItem item) { groupItem.replaceMember(oldItem, newItem);
item.replaceMember(oldItem, newItem);
} }
} catch (ItemNotFoundException e) { } catch (ItemNotFoundException e) {
// the group might not yet be registered, let's ignore this // 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.setEventPublisher(getEventPublisher());
genericItem.setStateDescriptionService(stateDescriptionService); genericItem.setStateDescriptionService(stateDescriptionService);
genericItem.setCommandDescriptionService(commandDescriptionService); genericItem.setCommandDescriptionService(commandDescriptionService);
genericItem.setUnitProvider(unitProvider);
genericItem.setItemStateConverter(itemStateConverter); 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) { private void addMembersToGroupItem(GroupItem groupItem) {
@ -216,9 +227,8 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
for (String groupName : groupItemNames) { for (String groupName : groupItemNames) {
if (groupName != null) { if (groupName != null) {
try { try {
Item groupItem = getItem(groupName); if (getItem(groupName) instanceof GroupItem groupItem) {
if (groupItem instanceof GroupItem groupItem1) { groupItem.removeMember(item);
groupItem1.removeMember(item);
} }
} catch (ItemNotFoundException e) { } catch (ItemNotFoundException e) {
// the group might not yet be registered, let's ignore this // the group might not yet be registered, let's ignore this
@ -234,16 +244,16 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
@Override @Override
protected void onRemoveElement(Item element) { protected void onRemoveElement(Item element) {
if (element instanceof GenericItem item) { if (element instanceof GenericItem genericItem) {
item.dispose(); genericItem.dispose();
} }
removeFromGroupItems(element, element.getGroupNames()); removeFromGroupItems(element, element.getGroupNames());
} }
@Override @Override
protected void beforeUpdateElement(Item existingElement) { protected void beforeUpdateElement(Item existingElement) {
if (existingElement instanceof GenericItem item) { if (existingElement instanceof GenericItem genericItem) {
item.dispose(); genericItem.dispose();
} }
} }
@ -291,21 +301,6 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
super.unsetReadyService(readyService); 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) @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
protected void setItemStateConverter(ItemStateConverter itemStateConverter) { protected void setItemStateConverter(ItemStateConverter itemStateConverter) {
this.itemStateConverter = itemStateConverter; this.itemStateConverter = itemStateConverter;
@ -442,17 +437,6 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
registryHooks.remove(hook); 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) @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
public void setStateDescriptionService(StateDescriptionService stateDescriptionService) { public void setStateDescriptionService(StateDescriptionService stateDescriptionService) {
this.stateDescriptionService = stateDescriptionService; this.stateDescriptionService = stateDescriptionService;
@ -495,4 +479,31 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
protected void unsetManagedProvider(ManagedItemProvider provider) { protected void unsetManagedProvider(ManagedItemProvider provider) {
super.unsetManagedProvider(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.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.service.CommandDescriptionService; import org.openhab.core.service.CommandDescriptionService;
import org.openhab.core.service.StateDescriptionService; import org.openhab.core.service.StateDescriptionService;
@ -83,8 +82,6 @@ public abstract class GenericItem implements ActiveItem {
private @Nullable CommandDescriptionService commandDescriptionService; private @Nullable CommandDescriptionService commandDescriptionService;
protected @Nullable UnitProvider unitProvider;
protected @Nullable ItemStateConverter itemStateConverter; protected @Nullable ItemStateConverter itemStateConverter;
public GenericItem(String type, String name) { public GenericItem(String type, String name) {
@ -176,7 +173,6 @@ public abstract class GenericItem implements ActiveItem {
this.eventPublisher = null; this.eventPublisher = null;
this.stateDescriptionService = null; this.stateDescriptionService = null;
this.commandDescriptionService = null; this.commandDescriptionService = null;
this.unitProvider = null;
this.itemStateConverter = null; this.itemStateConverter = null;
} }
@ -192,10 +188,6 @@ public abstract class GenericItem implements ActiveItem {
this.commandDescriptionService = commandDescriptionService; this.commandDescriptionService = commandDescriptionService;
} }
public void setUnitProvider(@Nullable UnitProvider unitProvider) {
this.unitProvider = unitProvider;
}
public void setItemStateConverter(@Nullable ItemStateConverter itemStateConverter) { public void setItemStateConverter(@Nullable ItemStateConverter itemStateConverter) {
this.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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.service.CommandDescriptionService; import org.openhab.core.service.CommandDescriptionService;
import org.openhab.core.service.StateDescriptionService; import org.openhab.core.service.StateDescriptionService;
@ -40,7 +39,7 @@ import org.slf4j.LoggerFactory;
* @author Kai Kreuzer - Initial contribution * @author Kai Kreuzer - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class GroupItem extends GenericItem implements StateChangeListener { public class GroupItem extends GenericItem implements StateChangeListener, MetadataAwareItem {
public static final String TYPE = "Group"; 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) { private void sendGroupStateUpdatedEvent(String memberName, State state) {
EventPublisher eventPublisher1 = this.eventPublisher; EventPublisher eventPublisher1 = this.eventPublisher;
if (eventPublisher1 != null) { if (eventPublisher1 != null) {
@ -457,4 +448,25 @@ public class GroupItem extends GenericItem implements StateChangeListener {
private boolean hasOwnState(GroupItem item) { private boolean hasOwnState(GroupItem item) {
return item.getFunction() != null && item.getBaseItem() != null; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem; import org.openhab.core.items.GenericItem;
import org.openhab.core.items.ItemFactory; import org.openhab.core.items.ItemFactory;
import org.openhab.core.items.ItemUtil; 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.RollershutterItem;
import org.openhab.core.library.items.StringItem; import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.items.SwitchItem; 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.Component;
import org.osgi.service.component.annotations.Reference;
/** /**
* {@link CoreItemFactory}-Implementation for the core ItemTypes * {@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 ROLLERSHUTTER = "Rollershutter";
public static final String STRING = "String"; public static final String STRING = "String";
public static final String SWITCH = "Switch"; public static final String SWITCH = "Switch";
private final UnitProvider unitProvider;
@Activate
public CoreItemFactory(final @Reference UnitProvider unitProvider) {
this.unitProvider = unitProvider;
}
@Override @Override
public @Nullable GenericItem createItem(@Nullable String itemTypeName, String itemName) { public @Nullable GenericItem createItem(@Nullable String itemTypeName, String itemName) {
@ -62,34 +71,21 @@ public class CoreItemFactory implements ItemFactory {
} }
String itemType = ItemUtil.getMainItemType(itemTypeName); String itemType = ItemUtil.getMainItemType(itemTypeName);
switch (itemType) { return switch (itemType) {
case CALL: case CALL -> new CallItem(itemName);
return new CallItem(itemName); case COLOR -> new ColorItem(itemName);
case COLOR: case CONTACT -> new ContactItem(itemName);
return new ColorItem(itemName); case DATETIME -> new DateTimeItem(itemName);
case CONTACT: case DIMMER -> new DimmerItem(itemName);
return new ContactItem(itemName); case IMAGE -> new ImageItem(itemName);
case DATETIME: case LOCATION -> new LocationItem(itemName);
return new DateTimeItem(itemName); case NUMBER -> new NumberItem(itemTypeName, itemName, unitProvider);
case DIMMER: case PLAYER -> new PlayerItem(itemName);
return new DimmerItem(itemName); case ROLLERSHUTTER -> new RollershutterItem(itemName);
case IMAGE: case STRING -> new StringItem(itemName);
return new ImageItem(itemName); case SWITCH -> new SwitchItem(itemName);
case LOCATION: default -> null;
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;
}
} }
@Override @Override

View File

@ -21,11 +21,15 @@ import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.GenericItem; import org.openhab.core.items.GenericItem;
import org.openhab.core.items.ItemUtil; 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.CoreItemFactory;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType; 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.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State; 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.StateDescriptionFragmentBuilder;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import org.openhab.core.types.util.UnitUtils; 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 * 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 * @author Kai Kreuzer - Initial contribution
*/ */
@NonNullByDefault @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, private static final List<Class<? extends State>> ACCEPTED_DATA_TYPES = List.of(DecimalType.class,
QuantityType.class, UnDefType.class); QuantityType.class, UnDefType.class);
private static final List<Class<? extends Command>> ACCEPTED_COMMAND_TYPES = List.of(DecimalType.class, private static final List<Class<? extends Command>> ACCEPTED_COMMAND_TYPES = List.of(DecimalType.class,
QuantityType.class, RefreshType.class); QuantityType.class, RefreshType.class);
@Nullable private final Logger logger = LoggerFactory.getLogger(NumberItem.class);
private Class<? extends Quantity<?>> dimension;
private final @Nullable Class<? extends Quantity<?>> dimension;
private Unit<?> unit = Units.ONE;
private final @Nullable UnitProvider unitProvider;
public NumberItem(String name) { 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); super(type, name);
this.unitProvider = unitProvider;
String itemTypeExtension = ItemUtil.getItemTypeExtension(getType()); String itemTypeExtension = ItemUtil.getItemTypeExtension(getType());
if (itemTypeExtension != null) { if (itemTypeExtension != null) {
dimension = UnitUtils.parseDimension(itemTypeExtension); 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()); DecimalType strippedCommand = new DecimalType(command.toBigDecimal());
internalSend(strippedCommand); internalSend(strippedCommand);
} else { } 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 @Override
public void setState(State state) { public void setState(State state) {
// QuantityType update to a NumberItem without, strip unit if (state instanceof QuantityType<?> quantityType) {
if (state instanceof QuantityType quantityType && dimension == null) { if (dimension == null) {
DecimalType plainState = new DecimalType(quantityType.toBigDecimal()); // QuantityType update to a NumberItem without unit, strip unit
super.setState(plainState); DecimalType plainState = new DecimalType(quantityType.toBigDecimal());
return; super.applyState(plainState);
} } else {
// QuantityType update to a NumberItem with unit, convert to item unit (if possible)
// DecimalType update for a NumberItem with dimension, convert to QuantityType: Unit<?> stateUnit = quantityType.getUnit();
if (state instanceof DecimalType decimalType && dimension != null) { State convertedState = (stateUnit.isCompatible(unit) || stateUnit.inverse().isCompatible(unit))
Unit<?> unit = getUnit(dimension, false); ? quantityType.toInvertibleUnit(unit)
if (unit != null) { : 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 (convertedState != null) { if (convertedState != null) {
super.setState(convertedState); super.applyState(convertedState);
return; } 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;
} }
} } else if (state instanceof DecimalType decimalType) {
if (dimension == null) {
if (isAcceptedState(ACCEPTED_DATA_TYPES, state)) { // DecimalType update to NumberItem with unit
super.setState(state); 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 { } else {
logSetTypeError(state); logSetTypeError(state);
} }
@ -160,83 +178,54 @@ public class NumberItem extends GenericItem {
* @return the optional unit symbol for this {@link NumberItem}. * @return the optional unit symbol for this {@link NumberItem}.
*/ */
public @Nullable String getUnitSymbol() { public @Nullable String getUnitSymbol() {
Unit<?> unit = getUnit(dimension, true); return (dimension != null) ? unit.toString() : null;
return unit != null ? unit.toString() : null;
} }
/** /**
* Derive the unit for this item by the following priority: * Get the unit for this item, either:
*
* <ul> * <ul>
* <li>the unit parsed from the state description</li> * <li>the unit retrieved from the <code>unit</code> namespace in the item's metadata</li>
* <li>no unit if state description contains <code>%unit%</code></li> * <li>the default system unit for the item's dimension</li>
* <li>the default system unit from the item's dimension</li>
* </ul> * </ul>
* *
* @return the {@link Unit} for this item if available, {@code null} otherwise. * @return the {@link Unit} for this item if available, {@code null} otherwise.
*/ */
public @Nullable Unit<? extends Quantity<?>> getUnit() { public @Nullable Unit<? extends Quantity<?>> getUnit() {
return getUnit(dimension, true); return (dimension != null) ? unit : null;
} }
/** @Override
* Try to convert a {@link DecimalType} into a new {@link QuantityType}. The unit for the new public void addedMetadata(Metadata metadata) {
* type is derived either from the state description (which might also give a hint on items w/o dimension) or from if (dimension != null && UNIT_METADATA_NAMESPACE.equals(metadata.getUID().getNamespace())) {
* the system default unit of the given dimension. Unit<?> unit = UnitUtils.parseUnit(metadata.getValue());
* if (unit == null) {
* @param originalType the source {@link DecimalType}. logger.warn("Unit '{}' could not be parsed to a known unit. Keeping old unit '{}' for item '{}'.",
* @param dimension the dimension to which the new {@link QuantityType} should adhere. metadata.getValue(), this.unit, name);
* @return the new {@link QuantityType} from the given originalType, {@code null} if a unit could not be calculated. return;
*/
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;
}
} }
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) { @Override
// explicit cast to Class<? extends Quantity> as JDK compiler complains public void updatedMetadata(Metadata oldMetadata, Metadata newMetadata) {
return unitProvider.getUnit((Class<? extends Quantity>) dimension); 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. * @return the new {@link QuantityType} in the given {@link Unit} or {@code null} in case of an erro.
*/ */
public @Nullable QuantityType<?> toInvertibleUnit(Unit<?> targetUnit) { 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 inverse().toUnit(targetUnit);
} }
return 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. * @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. * @throws IllegalArgumentException in case no class instance could be parsed from the given dimension.
*/ */
public static @Nullable Class<? extends Quantity<?>> parseDimension(String dimension) { public static @Nullable Class<? extends Quantity<?>> parseDimension(@Nullable String dimension) {
if (dimension.isBlank()) { if (dimension == null || dimension.isBlank()) {
return null; 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 * 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). * °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 * @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(); return quantity.getUnit();
} catch (IllegalArgumentException | MeasurementParseException e) { } catch (IllegalArgumentException | MeasurementParseException e) {
// we expect this exception in case the extracted string does not match any known unit // 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.mockito.junit.jupiter.MockitoExtension;
import org.openhab.core.events.Event; import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.internal.items.ExpireManager.ExpireConfig; import org.openhab.core.internal.items.ExpireManager.ExpireConfig;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException; 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.library.types.StringType;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import tech.units.indriya.unit.Units;
/** /**
* The {@link ExpireManagerTest} tests the {@link ExpireManager}. * The {@link ExpireManagerTest} tests the {@link ExpireManager}.
* *
@ -341,7 +344,9 @@ class ExpireManagerTest {
// expected as state is invalid // 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()); cfg = new ExpireManager.ExpireConfig(testItem, "1h,15 °C", Map.of());
assertEquals(Duration.ofHours(1), cfg.duration); assertEquals(Duration.ofHours(1), cfg.duration);
assertEquals(new QuantityType<Temperature>("15 °C"), cfg.expireState); 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.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.openhab.core.events.EventPublisher; 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.ItemEvent;
import org.openhab.core.items.events.ItemStateChangedEvent; import org.openhab.core.items.events.ItemStateChangedEvent;
import org.openhab.core.items.events.ItemStateUpdatedEvent; import org.openhab.core.items.events.ItemStateUpdatedEvent;
@ -160,7 +159,6 @@ public class GenericItemTest {
item.setEventPublisher(mock(EventPublisher.class)); item.setEventPublisher(mock(EventPublisher.class));
item.setItemStateConverter(mock(ItemStateConverter.class)); item.setItemStateConverter(mock(ItemStateConverter.class));
item.setStateDescriptionService(null); item.setStateDescriptionService(null);
item.setUnitProvider(mock(UnitProvider.class));
item.addStateChangeListener(mock(StateChangeListener.class)); item.addStateChangeListener(mock(StateChangeListener.class));
@ -170,7 +168,6 @@ public class GenericItemTest {
assertNull(item.itemStateConverter); assertNull(item.itemStateConverter);
// can not be tested as stateDescriptionProviders is private in GenericItem // can not be tested as stateDescriptionProviders is private in GenericItem
// assertThat(item.stateDescriptionProviders, is(nullValue())); // assertThat(item.stateDescriptionProviders, is(nullValue()));
assertNull(item.unitProvider);
assertEquals(0, item.listeners.size()); 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.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsEqual.equalTo;
import static org.mockito.Mockito.when;
import java.util.List; import java.util.List;
@ -22,18 +23,30 @@ import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test; 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.items.GenericItem;
import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.items.NumberItem;
import tech.units.indriya.unit.Units;
/** /**
* @author Henning Treu - Initial contribution * @author Henning Treu - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class CoreItemFactoryTest { public class CoreItemFactoryTest {
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
@Test @Test
public void shouldCreateItems() { public void shouldCreateItems() {
CoreItemFactory coreItemFactory = new CoreItemFactory(); CoreItemFactory coreItemFactory = new CoreItemFactory(unitProviderMock);
List<String> itemTypeNames = List.of(coreItemFactory.getSupportedItemTypes()); List<String> itemTypeNames = List.of(coreItemFactory.getSupportedItemTypes());
for (String itemTypeName : itemTypeNames) { for (String itemTypeName : itemTypeNames) {
GenericItem item = coreItemFactory.createItem(itemTypeName, itemTypeName.toLowerCase()); GenericItem item = coreItemFactory.createItem(itemTypeName, itemTypeName.toLowerCase());
@ -45,7 +58,8 @@ public class CoreItemFactoryTest {
@Test @Test
public void createNumberItemWithDimension() { 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", NumberItem numberItem = (NumberItem) coreItemFactory.createItem(CoreItemFactory.NUMBER + ":Temperature",
"myNumberItem"); "myNumberItem");
@ -54,7 +68,7 @@ public class CoreItemFactoryTest {
@Test @Test
public void shouldReturnNullForUnsupportedItemTypeName() { public void shouldReturnNullForUnsupportedItemTypeName() {
CoreItemFactory coreItemFactory = new CoreItemFactory(); CoreItemFactory coreItemFactory = new CoreItemFactory(unitProviderMock);
GenericItem item = coreItemFactory.createItem("NoValidItemTypeName", "IWantMyItem"); GenericItem item = coreItemFactory.createItem("NoValidItemTypeName", "IWantMyItem");
assertThat(item, is(nullValue())); 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.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat; 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 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 javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; 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.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider; 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.ItemCommandEvent;
import org.openhab.core.items.events.ItemStateUpdatedEvent;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType; 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.SIUnits;
import org.openhab.core.library.unit.Units; import org.openhab.core.library.unit.Units;
import org.openhab.core.service.StateDescriptionService; import org.openhab.core.service.StateDescriptionService;
@ -67,7 +65,10 @@ public class NumberItemTest {
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock; private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
private @Mock @NonNullByDefault({}) EventPublisher eventPublisherMock; private @Mock @NonNullByDefault({}) EventPublisher eventPublisherMock;
private final UnitProvider unitProvider = new TestUnitProvider();
@BeforeEach @BeforeEach
@SuppressWarnings("unchecked")
public void setup() { public void setup() {
when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)) when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null))
.thenReturn(StateDescriptionFragmentBuilder.create().withPattern("%.1f " + UnitUtils.UNIT_PLACEHOLDER) .thenReturn(StateDescriptionFragmentBuilder.create().withPattern("%.1f " + UnitUtils.UNIT_PLACEHOLDER)
@ -75,30 +76,9 @@ public class NumberItemTest {
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
} }
@Test /*
public void setDecimalType() { * State handling
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());
}
@Test @Test
public void testUndefType() { public void testUndefType() {
NumberItem item = new NumberItem(ITEM_NAME); NumberItem item = new NumberItem(ITEM_NAME);
@ -112,46 +92,76 @@ public class NumberItemTest {
} }
@Test @Test
public void testSetQuantityTypeAccepted() { public void testSetDecimalTypeToPlainItem() {
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() {
NumberItem item = new NumberItem(ITEM_NAME); NumberItem item = new NumberItem(ITEM_NAME);
item.setState(new QuantityType<>("20 °C")); State decimal = new DecimalType("23");
item.setState(decimal);
assertThat(item.getState(), is(new DecimalType("20"))); assertThat(item.getState(), is(decimal));
} }
@Test @Test
public void testSetQuantityTypeConverted() { public void testSetDecimalTypeToDimensionItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
item.setState(new QuantityType<>(68, ImperialUnits.FAHRENHEIT)); State decimal = new DecimalType("23");
item.setState(decimal);
assertThat(item.getState(), is(new QuantityType<>("20 °C"))); assertThat(item.getState(), is(new QuantityType<>("23 °C")));
} }
@Test @Test
public void testSetQuantityTypeUnconverted() { public void testSetQuantityTypeToPlainItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); NumberItem item = new NumberItem(ITEM_NAME);
UnitProvider unitProvider = mock(UnitProvider.class); State quantity = new QuantityType<>("23 °C");
when(unitProvider.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); item.setState(quantity);
item.setUnitProvider(unitProvider); assertThat(item.getState(), is(new DecimalType("23")));
item.setState(new QuantityType<>("10 A")); // should not be accepted as valid state }
@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)); assertThat(item.getState(), is(UnDefType.NULL));
} }
@Test @Test
public void testCommandUnitIsPassedForDimensionItem() { public void testSetPercentType() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); NumberItem item = new NumberItem(ITEM_NAME);
UnitProvider unitProvider = mock(UnitProvider.class); State percent = new PercentType(50);
when(unitProvider.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS); item.setState(percent);
item.setUnitProvider(unitProvider); 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); EventPublisher eventPublisher = mock(EventPublisher.class);
item.setEventPublisher(eventPublisher); item.setEventPublisher(eventPublisher);
@ -165,9 +175,37 @@ public class NumberItemTest {
assertThat(event.getItemCommand(), is(command)); 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 @Test
public void testCommandUnitIsStrippedForDimensionlessItem() { public void testCommandUnitIsStrippedForDimensionlessItem() {
NumberItem item = new NumberItem("Number", ITEM_NAME); NumberItem item = new NumberItem(ITEM_NAME);
EventPublisher eventPublisher = mock(EventPublisher.class); EventPublisher eventPublisher = mock(EventPublisher.class);
item.setEventPublisher(eventPublisher); item.setEventPublisher(eventPublisher);
@ -180,10 +218,13 @@ public class NumberItemTest {
assertThat(event.getItemCommand(), is(new DecimalType("15"))); assertThat(event.getItemCommand(), is(new DecimalType("15")));
} }
/*
* + State description handling
*/
@SuppressWarnings("null") @SuppressWarnings("null")
@Test @Test
public void testStripUnitPlaceholderFromPlainNumberItem() { public void testStripUnitPlaceholderInStateDescriptionFromPlainNumberItem() {
NumberItem item = new NumberItem("Number", ITEM_NAME); NumberItem item = new NumberItem(ITEM_NAME);
item.setStateDescriptionService(stateDescriptionServiceMock); item.setStateDescriptionService(stateDescriptionServiceMock);
assertThat(item.getStateDescription().getPattern(), is("%.1f")); assertThat(item.getStateDescription().getPattern(), is("%.1f"));
@ -191,20 +232,54 @@ public class NumberItemTest {
@SuppressWarnings("null") @SuppressWarnings("null")
@Test @Test
public void testLeaveUnitPlaceholderOnDimensionNumberItem() { public void testLeaveUnitPlaceholderInStateDescriptionOnDimensionNumberItem() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
item.setStateDescriptionService(stateDescriptionServiceMock); item.setStateDescriptionService(stateDescriptionServiceMock);
assertThat(item.getStateDescription().getPattern(), is("%.1f " + UnitUtils.UNIT_PLACEHOLDER)); 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") @SuppressWarnings("null")
@Test @Test
public void testMiredToKelvin() { public void testMiredToKelvin() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)).thenReturn( item.addedMetadata(getUnitMetadata(Units.KELVIN));
StateDescriptionFragmentBuilder.create().withPattern("%.0f K").build().toStateDescription());
item.setStateDescriptionService(stateDescriptionServiceMock);
item.setState(new QuantityType<>("370 mired")); item.setState(new QuantityType<>("370 mired"));
assertThat(item.getState().format("%.0f K"), is("2703 K")); assertThat(item.getState().format("%.0f K"), is("2703 K"));
@ -213,130 +288,16 @@ public class NumberItemTest {
@SuppressWarnings("null") @SuppressWarnings("null")
@Test @Test
public void testKelvinToMired() { public void testKelvinToMired() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME); NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME, unitProvider);
when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)).thenReturn( item.addedMetadata(getUnitMetadata(Units.MIRED));
StateDescriptionFragmentBuilder.create().withPattern("%.0f mired").build().toStateDescription());
item.setStateDescriptionService(stateDescriptionServiceMock);
item.setState(new QuantityType<>("2700 K")); item.setState(new QuantityType<>("2700 K"));
assertThat(item.getState().format("%.0f mired"), is("370 mired")); assertThat(item.getState().format("%.0f mired"), is("370 mired"));
} }
@Test private Metadata getUnitMetadata(Unit<?> unit) {
void testStateDescriptionUnitUsedWhenStateDescriptionPresent() { MetadataKey key = new MetadataKey(NumberItem.UNIT_METADATA_NAMESPACE, ITEM_NAME);
UnitProvider unitProviderMock = mock(UnitProvider.class); return new Metadata(key, unit.toString(), null);
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")));
} }
} }

View File

@ -32,6 +32,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.core.i18n.UnitProvider; import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.internal.i18n.TestUnitProvider;
import org.openhab.core.items.GroupFunction; import org.openhab.core.items.GroupFunction;
import org.openhab.core.items.GroupItem; import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item; 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.library.items.NumberItem;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import org.osgi.service.component.ComponentContext;
/** /**
* @author Henning Treu - Initial contribution * @author Henning Treu - Initial contribution
@ -47,7 +49,8 @@ import org.openhab.core.types.UnDefType;
@NonNullByDefault @NonNullByDefault
public class QuantityTypeArithmeticGroupFunctionTest { 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. * 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) { private NumberItem createNumberItem(String name, Class<? extends Quantity<?>> dimension, State state) {
NumberItem item = new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name); NumberItem item = new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name, unitProvider);
item.setUnitProvider(unitProvider);
item.setState(state); item.setState(state);
return item; return item;
} }
private GroupItem createGroupItem(String name, Class<? extends Quantity<?>> dimension, State state) { private GroupItem createGroupItem(String name, Class<? extends Quantity<?>> dimension, State state) {
GroupItem item = new GroupItem(name, GroupItem item = new GroupItem(name,
new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name)); new NumberItem(CoreItemFactory.NUMBER + ":" + dimension.getSimpleName(), name, unitProvider));
item.setUnitProvider(unitProvider);
item.setState(state); item.setState(state);
return item; 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-commons;version='[1.9.2,1.9.3)',\
junit-platform-engine;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)',\ 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.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
import java.util.Collection; import java.util.Collection;
import java.util.List; 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.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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.common.registry.ProviderChangeListener;
import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.items.ItemProvider; import org.openhab.core.items.ItemProvider;
import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemRegistry;
@ -48,6 +55,8 @@ import org.openhab.core.types.State;
* @author Henning Treu - Initial contribution * @author Henning Treu - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class ScriptEngineOSGiTest extends JavaOSGiTest { public class ScriptEngineOSGiTest extends JavaOSGiTest {
private static final String ITEM_NAME = "Switch1"; private static final String ITEM_NAME = "Switch1";
@ -58,9 +67,13 @@ public class ScriptEngineOSGiTest extends JavaOSGiTest {
private @NonNullByDefault({}) ItemProvider itemProvider; private @NonNullByDefault({}) ItemProvider itemProvider;
private @NonNullByDefault({}) ItemRegistry itemRegistry; private @NonNullByDefault({}) ItemRegistry itemRegistry;
private @NonNullByDefault({}) ScriptEngine scriptEngine; private @NonNullByDefault({}) ScriptEngine scriptEngine;
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
@BeforeEach @BeforeEach
public void setup() { public void setup() {
when(unitProviderMock.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
when(unitProviderMock.getUnit(Length.class)).thenReturn(SIUnits.METRE);
registerVolatileStorageService(); registerVolatileStorageService();
EventPublisher eventPublisher = event -> { EventPublisher eventPublisher = event -> {
@ -351,7 +364,7 @@ public class ScriptEngineOSGiTest extends JavaOSGiTest {
} }
private Item createNumberItem(String numberItemName, Class<?> dimension) { private Item createNumberItem(String numberItemName, Class<?> dimension) {
return new NumberItem("Number:" + dimension.getSimpleName(), numberItemName); return new NumberItem("Number:" + dimension.getSimpleName(), numberItemName, unitProviderMock);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

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

View File

@ -77,6 +77,7 @@ public class ItemRegistryImplTest extends JavaTest {
private @NonNullByDefault({}) ManagedItemProvider itemProvider; private @NonNullByDefault({}) ManagedItemProvider itemProvider;
private @Mock @NonNullByDefault({}) EventPublisher eventPublisherMock; private @Mock @NonNullByDefault({}) EventPublisher eventPublisherMock;
private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock;
@BeforeEach @BeforeEach
public void beforeEach() { public void beforeEach() {
@ -92,7 +93,7 @@ public class ItemRegistryImplTest extends JavaTest {
// setup ManageItemProvider with necessary dependencies: // setup ManageItemProvider with necessary dependencies:
itemProvider = new ManagedItemProvider(new VolatileStorageService(), itemProvider = new ManagedItemProvider(new VolatileStorageService(),
new ItemBuilderFactoryImpl(new CoreItemFactory())); new ItemBuilderFactoryImpl(new CoreItemFactory(unitProviderMock)));
itemProvider.add(new SwitchItem(ITEM_NAME)); itemProvider.add(new SwitchItem(ITEM_NAME));
itemProvider.add(cameraItem1); itemProvider.add(cameraItem1);
@ -107,7 +108,6 @@ public class ItemRegistryImplTest extends JavaTest {
setManagedProvider(itemProvider); setManagedProvider(itemProvider);
setEventPublisher(ItemRegistryImplTest.this.eventPublisherMock); setEventPublisher(ItemRegistryImplTest.this.eventPublisherMock);
setStateDescriptionService(mock(StateDescriptionService.class)); setStateDescriptionService(mock(StateDescriptionService.class));
setUnitProvider(mock(UnitProvider.class));
setItemStateConverter(mock(ItemStateConverter.class)); setItemStateConverter(mock(ItemStateConverter.class));
} }
}; };
@ -369,13 +369,11 @@ public class ItemRegistryImplTest extends JavaTest {
assertNotNull(item.eventPublisher); assertNotNull(item.eventPublisher);
assertNotNull(item.itemStateConverter); assertNotNull(item.itemStateConverter);
assertNotNull(item.unitProvider);
itemProvider.update(new SwitchItem("Item1")); itemProvider.update(new SwitchItem("Item1"));
assertNull(item.eventPublisher); assertNull(item.eventPublisher);
assertNull(item.itemStateConverter); assertNull(item.itemStateConverter);
assertNull(item.unitProvider);
assertEquals(0, item.listeners.size()); assertEquals(0, item.listeners.size());
} }
@ -391,18 +389,6 @@ public class ItemRegistryImplTest extends JavaTest {
verify(baseItem).setStateDescriptionService(any(StateDescriptionService.class)); 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 @Test
public void assertCommandDescriptionServiceGetsInjected() { public void assertCommandDescriptionServiceGetsInjected() {
GenericItem item = spy(new SwitchItem("Item1")); 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.Item;
import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.ItemStateConverter; 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.ItemCommandEvent;
import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.library.CoreItemFactory; 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.OnOffType;
import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType; 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.library.unit.SIUnits;
import org.openhab.core.service.StateDescriptionService;
import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.test.java.JavaOSGiTest;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; 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.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.State; 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 EVENT = "event";
private static final String ITEM_NAME_1 = "testItem1"; private static final String ITEM_NAME_1 = "testItem1";
private static final String ITEM_NAME_2 = "testItem2"; private static final String ITEM_NAME_2 = "testItem2";
private static final String ITEM_NAME_3 = "testItem3"; private static final String ITEM_NAME_3 = "testItem3";
private static final String ITEM_NAME_4 = "testItem4"; 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_1 = new SwitchItem(ITEM_NAME_1);
private static final SwitchItem ITEM_2 = new SwitchItem(ITEM_NAME_2); 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_3 = new NumberItem(ITEM_NAME_3);
private static final NumberItem ITEM_4 = new NumberItem(ITEM_NAME_4); 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 ThingTypeUID THING_TYPE_UID = new ThingTypeUID("test", "type");
private static final ThingUID THING_UID = new ThingUID("test", "thing"); 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_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_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_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_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 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_1 = new ChannelUID(THING_UID, "trigger-channel1");
private static final ChannelUID TRIGGER_CHANNEL_UID_2 = new ChannelUID(THING_UID, "trigger-channel2"); 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_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_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_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_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_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); 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_3, "Number:Temperature").withKind(ChannelKind.STATE).build(),
ChannelBuilder.create(STATE_CHANNEL_UID_4, CoreItemFactory.NUMBER).withKind(ChannelKind.STATE) ChannelBuilder.create(STATE_CHANNEL_UID_4, CoreItemFactory.NUMBER).withKind(ChannelKind.STATE)
.withType(CHANNEL_TYPE_UID_4).build(), .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_1).withKind(ChannelKind.TRIGGER).build(),
ChannelBuilder.create(TRIGGER_CHANNEL_UID_2).withKind(ChannelKind.TRIGGER).build()).build(); ChannelBuilder.create(TRIGGER_CHANNEL_UID_2).withKind(ChannelKind.TRIGGER).build()).build();
@ -158,6 +167,9 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@BeforeEach @BeforeEach
public void 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); safeCaller = getService(SafeCaller.class);
assertNotNull(safeCaller); assertNotNull(safeCaller);
@ -166,7 +178,8 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
assertNotNull(profileFactory); assertNotNull(profileFactory);
manager = new CommunicationManager(autoUpdateManagerMock, channelTypeRegistryMock, profileFactory, iclRegistry, manager = new CommunicationManager(autoUpdateManagerMock, channelTypeRegistryMock, profileFactory, iclRegistry,
itemRegistryMock, itemStateConverterMock, eventPublisherMock, safeCaller, thingRegistryMock); itemRegistryMock, itemStateConverterMock, eventPublisherMock, safeCaller, thingRegistryMock,
unitProviderMock);
doAnswer(invocation -> { doAnswer(invocation -> {
switch (((Channel) invocation.getArguments()[0]).getKind()) { switch (((Channel) invocation.getArguments()[0]).getKind()) {
@ -205,7 +218,8 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@Override @Override
public Collection<ItemChannelLink> getAll() { 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_2))).thenReturn(ITEM_2);
when(itemRegistryMock.get(eq(ITEM_NAME_3))).thenReturn(ITEM_3); 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_4))).thenReturn(ITEM_4);
when(itemRegistryMock.get(eq(ITEM_NAME_5))).thenReturn(ITEM_5);
ChannelType channelType4 = mock(ChannelType.class); ChannelType channelType4 = mock(ChannelType.class);
when(channelType4.getItemType()).thenReturn("Number:Temperature"); when(channelType4.getItemType()).thenReturn("Number:Temperature");
@ -222,12 +237,8 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
THING.setHandler(thingHandlerMock); THING.setHandler(thingHandlerMock);
when(thingRegistryMock.get(eq(THING_UID))).thenReturn(THING); when(thingRegistryMock.get(eq(THING_UID))).thenReturn(THING);
manager.addItemFactory(new CoreItemFactory());
UnitProvider unitProvider = mock(UnitProvider.class); manager.addItemFactory(new CoreItemFactory(unitProviderMock));
when(unitProvider.getUnit(Temperature.class)).thenReturn(SIUnits.CELSIUS);
ITEM_3.setUnitProvider(unitProvider);
ITEM_4.setUnitProvider(unitProvider);
} }
@Test @Test
@ -284,7 +295,7 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@Test @Test
public void testItemCommandEventDecimal2Quantity() { public void testItemCommandEventDecimal2Quantity() {
// Take unit from accepted item type (see channel built from STATE_CHANNEL_UID_3) // 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(() -> { waitForAssert(() -> {
verify(stateProfileMock).onCommandFromItem(eq(QuantityType.valueOf("20 °C"))); verify(stateProfileMock).onCommandFromItem(eq(QuantityType.valueOf("20 °C")));
}); });
@ -294,33 +305,16 @@ public class CommunicationManagerOSGiTest extends JavaOSGiTest {
@Test @Test
public void testItemCommandEventDecimal2Quantity2() { public void testItemCommandEventDecimal2Quantity2() {
// Take unit from state description MetadataKey key = new MetadataKey(NumberItem.UNIT_METADATA_NAMESPACE, ITEM_NAME_5);
StateDescriptionService stateDescriptionService = mock(StateDescriptionService.class); Metadata metadata = new Metadata(key, ImperialUnits.FAHRENHEIT.toString(), null);
when(stateDescriptionService.getStateDescription(ITEM_NAME_3, null)).thenReturn( ITEM_5.addedMetadata(metadata);
StateDescriptionFragmentBuilder.create().withPattern("%.1f °F").build().toStateDescription());
ITEM_3.setStateDescriptionService(stateDescriptionService);
manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_3, DecimalType.valueOf("20"))); manager.receive(ItemEventFactory.createCommandEvent(ITEM_NAME_5, DecimalType.valueOf("20")));
waitForAssert(() -> { waitForAssert(() -> {
verify(stateProfileMock).onCommandFromItem(eq(QuantityType.valueOf("20 °F"))); verify(stateProfileMock).onCommandFromItem(eq(QuantityType.valueOf("20 °F")));
}); });
verifyNoMoreInteractions(stateProfileMock); verifyNoMoreInteractions(stateProfileMock);
verifyNoMoreInteractions(triggerProfileMock); 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 @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.ManagedItemProvider;
import org.openhab.core.items.Metadata; import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataKey; 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.storage.json.internal.JsonStorage;
import org.openhab.core.thing.internal.link.ItemChannelLinkConfigDescriptionProvider; import org.openhab.core.thing.internal.link.ItemChannelLinkConfigDescriptionProvider;
import org.openhab.core.thing.link.ItemChannelLink; import org.openhab.core.thing.link.ItemChannelLink;
@ -91,7 +92,7 @@ public class Upgrader {
itemStorage.getKeys().forEach(itemName -> { itemStorage.getKeys().forEach(itemName -> {
ManagedItemProvider.PersistedItem item = itemStorage.get(itemName); ManagedItemProvider.PersistedItem item = itemStorage.get(itemName);
if (item != null && item.itemType.startsWith("Number:")) { 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); logger.debug("{}: already contains a 'unit' metadata, skipping it", itemName);
} else { } else {
Metadata metadata = metadataStorage.get("stateDescription:" + itemName); Metadata metadata = metadataStorage.get("stateDescription:" + itemName);
@ -107,7 +108,8 @@ public class Upgrader {
Unit<?> stateDescriptionUnit = UnitUtils.parseUnit(pattern); Unit<?> stateDescriptionUnit = UnitUtils.parseUnit(pattern);
if (stateDescriptionUnit != null) { if (stateDescriptionUnit != null) {
String unit = stateDescriptionUnit.toString(); 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); Metadata defaultUnitMetadata = new Metadata(defaultUnitMetadataKey, unit, null);
metadataStorage.put(defaultUnitMetadataKey.toString(), defaultUnitMetadata); metadataStorage.put(defaultUnitMetadataKey.toString(), defaultUnitMetadata);
logger.info("{}: Wrote 'unit={}' to metadata.", itemName, unit); logger.info("{}: Wrote 'unit={}' to metadata.", itemName, unit);