forked from openhab/openhab-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Metadata-based state & command description providers (openhab#1362)
* 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
Showing
4 changed files
with
477 additions
and
0 deletions.
There are no files selected for viewing
84 changes: 84 additions & 0 deletions
84
...ore/src/main/java/org/openhab/core/internal/items/MetadataCommandDescriptionProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
151 changes: 151 additions & 0 deletions
151
...c/main/java/org/openhab/core/internal/items/MetadataStateDescriptionFragmentProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
120 changes: 120 additions & 0 deletions
120
...src/test/java/org/openhab/core/internal/items/MetadataCommandDescriptionProviderTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
Oops, something went wrong.