diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataCommandDescriptionProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataCommandDescriptionProvider.java new file mode 100644 index 0000000000..10ff49a006 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataCommandDescriptionProvider.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2020 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.items; + +import java.util.Locale; +import java.util.Map; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.internal.types.CommandDescriptionImpl; +import org.openhab.core.items.Metadata; +import org.openhab.core.items.MetadataKey; +import org.openhab.core.items.MetadataRegistry; +import org.openhab.core.types.CommandDescription; +import org.openhab.core.types.CommandDescriptionProvider; +import org.openhab.core.types.CommandOption; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link CommandDescription} provider from items' metadata + * + * @author Yannick Schaus - initial contribution + * + */ +@NonNullByDefault +@Component(service = CommandDescriptionProvider.class) +public class MetadataCommandDescriptionProvider implements CommandDescriptionProvider { + + private final Logger logger = LoggerFactory.getLogger(MetadataCommandDescriptionProvider.class); + + public static final String COMMANDDESCRIPTION_METADATA_NAMESPACE = "commandDescription"; + + private MetadataRegistry metadataRegistry; + + @Activate + public MetadataCommandDescriptionProvider(final @Reference MetadataRegistry metadataRegistry, + Map properties) { + this.metadataRegistry = metadataRegistry; + } + + @Override + public @Nullable CommandDescription getCommandDescription(@NonNull String itemName, @Nullable Locale locale) { + Metadata metadata = metadataRegistry.get(new MetadataKey(COMMANDDESCRIPTION_METADATA_NAMESPACE, itemName)); + + if (metadata != null) { + try { + CommandDescriptionImpl commandDescription = new CommandDescriptionImpl(); + if (metadata.getConfiguration().containsKey("options")) { + Stream.of(metadata.getConfiguration().get("options").toString().split(",")).forEach(o -> { + if (o.contains("=")) { + commandDescription.addCommandOption( + new CommandOption(o.split("=")[0].trim(), o.split("=")[1].trim())); + } else { + commandDescription.addCommandOption(new CommandOption(o.trim(), null)); + } + }); + + return commandDescription; + } + } catch (Exception e) { + logger.warn("Unable to parse the commandDescription from metadata for item {}, ignoring it", itemName); + return null; + } + } + + return null; + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProvider.java new file mode 100644 index 0000000000..dcb6ff6b21 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProvider.java @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2010-2020 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.items; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.items.Metadata; +import org.openhab.core.items.MetadataKey; +import org.openhab.core.items.MetadataRegistry; +import org.openhab.core.types.StateDescriptionFragment; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.openhab.core.types.StateDescriptionFragmentProvider; +import org.openhab.core.types.StateOption; +import org.osgi.framework.Constants; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link StateDescriptionFragment} provider from items' metadata + * + * @author Yannick Schaus - initial contribution + * + */ +@NonNullByDefault +@Component(service = StateDescriptionFragmentProvider.class) +public class MetadataStateDescriptionFragmentProvider implements StateDescriptionFragmentProvider { + + private final Logger logger = LoggerFactory.getLogger(MetadataStateDescriptionFragmentProvider.class); + + public static final String STATEDESCRIPTION_METADATA_NAMESPACE = "stateDescription"; + + private final MetadataRegistry metadataRegistry; + + private final Integer rank; + + @Activate + public MetadataStateDescriptionFragmentProvider(final @Reference MetadataRegistry metadataRegistry, + Map properties) { + this.metadataRegistry = metadataRegistry; + + Object serviceRanking = properties.get(Constants.SERVICE_RANKING); + if (serviceRanking instanceof Integer) { + rank = (Integer) serviceRanking; + } else { + rank = 1; // takes precedence over other providers usually ranked 0 + } + } + + @Override + public @Nullable StateDescriptionFragment getStateDescriptionFragment(@NonNull String itemName, + @Nullable Locale locale) { + Metadata metadata = metadataRegistry.get(new MetadataKey(STATEDESCRIPTION_METADATA_NAMESPACE, itemName)); + + if (metadata != null) { + try { + StateDescriptionFragmentBuilder builder = StateDescriptionFragmentBuilder.create(); + if (metadata.getConfiguration().containsKey("pattern")) { + builder.withPattern((String) metadata.getConfiguration().get("pattern")); + } + if (metadata.getConfiguration().containsKey("min")) { + builder.withMinimum(getBigDecimal(metadata.getConfiguration().get("min"))); + } + if (metadata.getConfiguration().containsKey("max")) { + builder.withMaximum(getBigDecimal(metadata.getConfiguration().get("max"))); + } + if (metadata.getConfiguration().containsKey("step")) { + builder.withStep(getBigDecimal(metadata.getConfiguration().get("step"))); + } + if (metadata.getConfiguration().containsKey("readOnly")) { + builder.withReadOnly(getBoolean(metadata.getConfiguration().get("readOnly"))); + } + if (metadata.getConfiguration().containsKey("options")) { + List stateOptions = Stream + .of(metadata.getConfiguration().get("options").toString().split(",")).map(o -> { + return (o.contains("=")) + ? new StateOption(o.split("=")[0].trim(), o.split("=")[1].trim()) + : new StateOption(o.trim(), null); + }).collect(Collectors.toList()); + builder.withOptions(stateOptions); + } + + return builder.build(); + } catch (Exception e) { + logger.warn("Unable to parse the stateDescription from metadata for item {}, ignoring it", itemName); + } + } + + return null; + } + + private BigDecimal getBigDecimal(Object value) { + BigDecimal ret = null; + if (value != null) { + if (value instanceof BigDecimal) { + ret = (BigDecimal) value; + } else if (value instanceof String) { + ret = new BigDecimal((String) value); + } else if (value instanceof BigInteger) { + ret = new BigDecimal((BigInteger) value); + } else if (value instanceof Number) { + ret = new BigDecimal(((Number) value).doubleValue()); + } else { + throw new ClassCastException("Not possible to coerce [" + value + "] from class " + value.getClass() + + " into a BigDecimal."); + } + } + return ret; + } + + private Boolean getBoolean(Object value) { + Boolean ret = null; + if (value != null) { + if (value instanceof Boolean) { + ret = (Boolean) value; + } else if (value instanceof String) { + ret = Boolean.parseBoolean((String) value); + } else { + throw new ClassCastException( + "Not possible to coerce [" + value + "] from class " + value.getClass() + " into a Boolean."); + } + } + return ret; + } + + @Override + public @NonNull Integer getRank() { + return rank; + } +} diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataCommandDescriptionProviderTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataCommandDescriptionProviderTest.java new file mode 100644 index 0000000000..ec205224d1 --- /dev/null +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataCommandDescriptionProviderTest.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2010-2020 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.items; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.openhab.core.items.Item; +import org.openhab.core.items.ManagedMetadataProvider; +import org.openhab.core.items.Metadata; +import org.openhab.core.items.MetadataKey; +import org.openhab.core.types.CommandDescription; +import org.openhab.core.types.CommandOption; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * @author Yannick Schaus - Initial contribution + */ +public class MetadataCommandDescriptionProviderTest { + + private static final String ITEM_NAME = "itemName"; + + @SuppressWarnings("rawtypes") + private @Mock ServiceReference managedProviderRef; + private @Mock BundleContext bundleContext; + private @Mock ManagedMetadataProvider managedProvider; + private @Mock Item item; + + private @Mock MetadataRegistryImpl metadataRegistry; + private MetadataCommandDescriptionProvider commandDescriptionProvider; + + private ServiceListener providerTracker; + + @Before + @SuppressWarnings("unchecked") + public void setup() throws Exception { + initMocks(this); + + when(bundleContext.getService(same(managedProviderRef))).thenReturn(managedProvider); + + when(item.getName()).thenReturn(ITEM_NAME); + + metadataRegistry = new MetadataRegistryImpl(); + + metadataRegistry.setManagedProvider(managedProvider); + metadataRegistry.activate(bundleContext); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServiceListener.class); + verify(bundleContext).addServiceListener(captor.capture(), any()); + providerTracker = captor.getValue(); + providerTracker.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, managedProviderRef)); + + commandDescriptionProvider = new MetadataCommandDescriptionProvider(metadataRegistry, new HashMap<>()); + } + + @Test + public void testEmpty() throws Exception { + CommandDescription commandDescription = commandDescriptionProvider.getCommandDescription(ITEM_NAME, null); + assertNull(commandDescription); + } + + @Test + public void testEmptyConfig() throws Exception { + MetadataKey metadataKey = new MetadataKey("commandDescription", ITEM_NAME); + // Map metadataConfig = new HashMap<>(); + Metadata metadata = new Metadata(metadataKey, "N/A", null); + + metadataRegistry.added(managedProvider, metadata); + CommandDescription commandDescription = commandDescriptionProvider.getCommandDescription(ITEM_NAME, null); + assertNull(commandDescription); + } + + @Test + public void testOptions() throws Exception { + MetadataKey metadataKey = new MetadataKey("commandDescription", ITEM_NAME); + Map metadataConfig = new HashMap<>(); + metadataConfig.put("options", "OPTION1,OPTION2 , 3 =Option 3 "); + Metadata metadata = new Metadata(metadataKey, "N/A", metadataConfig); + + metadataRegistry.added(managedProvider, metadata); + CommandDescription commandDescription = commandDescriptionProvider.getCommandDescription(ITEM_NAME, null); + assertNotNull(commandDescription); + assertNotNull(commandDescription.getCommandOptions()); + assertEquals(3, commandDescription.getCommandOptions().size()); + + Iterator it = commandDescription.getCommandOptions().iterator(); + CommandOption commandOption = it.next(); + assertEquals("OPTION1", commandOption.getCommand()); + assertEquals(null, commandOption.getLabel()); + commandOption = it.next(); + assertEquals("OPTION2", commandOption.getCommand()); + assertEquals(null, commandOption.getLabel()); + commandOption = it.next(); + assertEquals("3", commandOption.getCommand()); + assertEquals("Option 3", commandOption.getLabel()); + } +} diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProviderTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProviderTest.java new file mode 100644 index 0000000000..4dbe6dec67 --- /dev/null +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProviderTest.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010-2020 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.items; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.openhab.core.items.Item; +import org.openhab.core.items.ManagedMetadataProvider; +import org.openhab.core.items.Metadata; +import org.openhab.core.items.MetadataKey; +import org.openhab.core.types.StateDescriptionFragment; +import org.openhab.core.types.StateOption; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * @author Yannick Schaus - Initial contribution + */ +public class MetadataStateDescriptionFragmentProviderTest { + + private static final String ITEM_NAME = "itemName"; + + @SuppressWarnings("rawtypes") + private @Mock ServiceReference managedProviderRef; + private @Mock BundleContext bundleContext; + private @Mock ManagedMetadataProvider managedProvider; + private @Mock Item item; + + private @Mock MetadataRegistryImpl metadataRegistry; + private MetadataStateDescriptionFragmentProvider stateDescriptionFragmentProvider; + + private ServiceListener providerTracker; + + @Before + @SuppressWarnings("unchecked") + public void setup() throws Exception { + initMocks(this); + + when(bundleContext.getService(same(managedProviderRef))).thenReturn(managedProvider); + + when(item.getName()).thenReturn(ITEM_NAME); + + metadataRegistry = new MetadataRegistryImpl(); + + metadataRegistry.setManagedProvider(managedProvider); + metadataRegistry.activate(bundleContext); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServiceListener.class); + verify(bundleContext).addServiceListener(captor.capture(), any()); + providerTracker = captor.getValue(); + providerTracker.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, managedProviderRef)); + + stateDescriptionFragmentProvider = new MetadataStateDescriptionFragmentProvider(metadataRegistry, + new HashMap<>()); + } + + @Test + public void testEmpty() throws Exception { + StateDescriptionFragment stateDescriptionFragment = stateDescriptionFragmentProvider + .getStateDescriptionFragment(ITEM_NAME, null); + assertNull(stateDescriptionFragment); + } + + @SuppressWarnings("null") + @Test + public void testFragment() throws Exception { + MetadataKey metadataKey = new MetadataKey("stateDescription", ITEM_NAME); + Map metadataConfig = new HashMap<>(); + metadataConfig.put("pattern", "%.1f %unit%"); + metadataConfig.put("min", 18.5); + metadataConfig.put("max", "34"); + metadataConfig.put("step", 3); + metadataConfig.put("readOnly", "true"); + metadataConfig.put("options", "OPTION1,OPTION2 , 3 =Option 3 "); + Metadata metadata = new Metadata(metadataKey, "N/A", metadataConfig); + metadataRegistry.added(managedProvider, metadata); + + StateDescriptionFragment stateDescriptionFragment = stateDescriptionFragmentProvider + .getStateDescriptionFragment(ITEM_NAME, null); + assertNotNull(stateDescriptionFragment); + assertEquals("%.1f %unit%", stateDescriptionFragment.getPattern()); + assertEquals(new BigDecimal(18.5), stateDescriptionFragment.getMinimum()); + assertEquals(new BigDecimal(34), stateDescriptionFragment.getMaximum()); + assertEquals(new BigDecimal(3), stateDescriptionFragment.getStep()); + assertEquals(true, stateDescriptionFragment.isReadOnly()); + assertNotNull(stateDescriptionFragment.getOptions()); + Iterator it = stateDescriptionFragment.getOptions().iterator(); + StateOption stateOption = it.next(); + assertEquals("OPTION1", stateOption.getValue()); + assertEquals(null, stateOption.getLabel()); + stateOption = it.next(); + assertEquals("OPTION2", stateOption.getValue()); + assertEquals(null, stateOption.getLabel()); + stateOption = it.next(); + assertEquals("3", stateOption.getValue()); + assertEquals("Option 3", stateOption.getLabel()); + } +}