Metadata-based state & command description providers (#1362)
* Metadata-based state & command description providers Implements #1185. These providers will look into item metadata, which can be managed by UIs with the API, to set or override the item's state description (pattern, options, read only...) or command description. Signed-off-by: Yannick Schaus <github@schaus.net>pull/1368/head
parent
76153dcab0
commit
60e040c529
|
@ -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<String, Object> 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;
|
||||
}
|
||||
}
|
|
@ -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<String, Object> 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<StateOption> 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;
|
||||
}
|
||||
}
|
|
@ -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<ServiceListener> 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<String, Object> 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<String, Object> 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<CommandOption> 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());
|
||||
}
|
||||
}
|
|
@ -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<ServiceListener> 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<String, Object> 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<StateOption> 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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue