Skip to content

Commit

Permalink
Metadata-based state & command description providers (openhab#1362)
Browse files Browse the repository at this point in the history
* Metadata-based state & command description providers

Implements openhab#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 <[email protected]>
  • Loading branch information
ghys authored Feb 15, 2020
1 parent 76153dc commit 60e040c
Show file tree
Hide file tree
Showing 4 changed files with 477 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading

0 comments on commit 60e040c

Please sign in to comment.