Skip to content

Commit

Permalink
Set a default presentation pattern for String/Number/Datetime items (#…
Browse files Browse the repository at this point in the history
…4175)

* Set a default presentation pattern for String/Number/Datetime items

A default state pattern was previously provided by ChannelStateDescriptionProvider only for String and Number items linked to a channel.
It is now the class DefaultStateDescriptionFragmentProvider which is responsible for providing the default state pattern for items, whether the item is linked to a channel or not.
This new class is the lowest ranked StateDescriptionFragmentProvider so that all other providers have priority in setting the state pattern.

Default pattern for string item: %s
Default pattern for datetime item or group with datetime state: %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS
Default pattern for number item or group with number state: %.0f
Default pattern for number+dimension item or group with number+dimension state: %.0f %unit%

Closes #4071
Closes #3835

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
  • Loading branch information
lolodomo committed May 2, 2024
1 parent 1b503af commit 1fb949b
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 19 deletions.
Expand Up @@ -21,7 +21,6 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.Item;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingRegistry;
Expand Down Expand Up @@ -100,22 +99,6 @@ public Integer getRank() {
ChannelType channelType = thingTypeRegistry.getChannelType(channel, locale);
if (channelType != null) {
stateDescription = channelType.getState();
String itemType = channelType.getItemType();
if (itemType != null && (stateDescription == null || stateDescription.getPattern() == null)) {
String pattern = null;
if (CoreItemFactory.STRING.equalsIgnoreCase(itemType)) {
pattern = "%s";
} else if (itemType.startsWith(CoreItemFactory.NUMBER)) {
pattern = "%.0f";
}
if (pattern != null) {
logger.trace("Provide a default pattern {} for item {}", pattern, itemName);
StateDescriptionFragmentBuilder builder = (stateDescription == null)
? StateDescriptionFragmentBuilder.create()
: StateDescriptionFragmentBuilder.create(stateDescription);
stateDescription = builder.withPattern(pattern).build().toStateDescription();
}
}
}
StateDescription dynamicStateDescription = getDynamicStateDescription(channel, stateDescription,
locale);
Expand Down
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2024 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.concurrent.ConcurrentHashMap;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.types.StateDescriptionFragment;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.StateDescriptionFragmentProvider;
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.Deactivate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A {@link StateDescriptionFragment} provider providing a default state pattern for items of type String,
* DateTime and Number (with or without dimension).
*
* @author Laurent Garnier - initial contribution
*
*/
@NonNullByDefault
@Component(service = { StateDescriptionFragmentProvider.class,
DefaultStateDescriptionFragmentProvider.class }, immediate = true, property = { "service.ranking:Integer=-2" })
public class DefaultStateDescriptionFragmentProvider implements StateDescriptionFragmentProvider {

private static final StateDescriptionFragment DEFAULT_STRING = StateDescriptionFragmentBuilder.create()
.withPattern("%s").build();
private static final StateDescriptionFragment DEFAULT_DATETIME = StateDescriptionFragmentBuilder.create()
.withPattern("%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS").build();
private static final StateDescriptionFragment DEFAULT_NUMBER = StateDescriptionFragmentBuilder.create()
.withPattern("%.0f").build();
private static final StateDescriptionFragment DEFAULT_NUMBER_WITH_DIMENSION = StateDescriptionFragmentBuilder
.create().withPattern("%.0f %unit%").build();

private final Logger logger = LoggerFactory.getLogger(DefaultStateDescriptionFragmentProvider.class);

private final Map<String, StateDescriptionFragment> stateDescriptionFragments = new ConcurrentHashMap<>();

private Integer rank = -2; // takes less precedence than all other providers

@Activate
public DefaultStateDescriptionFragmentProvider(Map<String, Object> properties) {
Object serviceRanking = properties.get(Constants.SERVICE_RANKING);
if (serviceRanking instanceof Integer rankValue) {
rank = rankValue;
}
}

@Deactivate
protected void deactivate() {
stateDescriptionFragments.clear();
}

public void onItemAdded(Item item) {
logger.trace("onItemAdded {} {}", item.getName(), item.getType());
if (item instanceof GroupItem group) {
Item baseItem = group.getBaseItem();
if (baseItem != null) {
onItemAdded(baseItem);
}
} else if (item.getType().startsWith(CoreItemFactory.NUMBER + ":")) {
stateDescriptionFragments.put(item.getName(), DEFAULT_NUMBER_WITH_DIMENSION);
} else {
switch (item.getType()) {
case CoreItemFactory.STRING:
stateDescriptionFragments.put(item.getName(), DEFAULT_STRING);
break;
case CoreItemFactory.DATETIME:
stateDescriptionFragments.put(item.getName(), DEFAULT_DATETIME);
break;
case CoreItemFactory.NUMBER:
stateDescriptionFragments.put(item.getName(), DEFAULT_NUMBER);
break;
default:
stateDescriptionFragments.remove(item.getName());
}
}
}

public void onItemRemoved(Item item) {
logger.trace("onItemRemoved {}", item.getName());
stateDescriptionFragments.remove(item.getName());
}

@Override
public @Nullable StateDescriptionFragment getStateDescriptionFragment(String itemName, @Nullable Locale locale) {
return stateDescriptionFragments.get(itemName);
}

@Override
public Integer getRank() {
return rank;
}
}
Expand Up @@ -59,6 +59,7 @@
*
* @author Kai Kreuzer - Initial contribution
* @author Stefan Bußweiler - Migration to new event mechanism
* @author Laurent Garnier - handle new DefaultStateDescriptionFragmentProvider
*/
@NonNullByDefault
@Component(immediate = true)
Expand All @@ -71,13 +72,16 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
private @Nullable StateDescriptionService stateDescriptionService;
private @Nullable CommandDescriptionService commandDescriptionService;
private final MetadataRegistry metadataRegistry;
private final DefaultStateDescriptionFragmentProvider defaultStateDescriptionFragmentProvider;

private @Nullable ItemStateConverter itemStateConverter;

@Activate
public ItemRegistryImpl(final @Reference MetadataRegistry metadataRegistry) {
public ItemRegistryImpl(final @Reference MetadataRegistry metadataRegistry,
final @Reference DefaultStateDescriptionFragmentProvider defaultStateDescriptionFragmentProvider) {
super(ItemProvider.class);
this.metadataRegistry = metadataRegistry;
this.defaultStateDescriptionFragmentProvider = defaultStateDescriptionFragmentProvider;
}

@Activate
Expand Down Expand Up @@ -198,6 +202,8 @@ private void initializeItem(Item item) throws IllegalArgumentException {

// add the item to all relevant groups
addToGroupItems(item, item.getGroupNames());

defaultStateDescriptionFragmentProvider.onItemAdded(item);
}

private void injectServices(Item item) {
Expand Down Expand Up @@ -246,6 +252,7 @@ protected void onRemoveElement(Item element) {
genericItem.dispose();
}
removeFromGroupItems(element, element.getGroupNames());
defaultStateDescriptionFragmentProvider.onItemRemoved(element);
}

@Override
Expand All @@ -269,6 +276,9 @@ protected void onUpdateElement(Item oldItem, Item item) {
addMembersToGroupItem(groupItem);
}
injectServices(item);

defaultStateDescriptionFragmentProvider.onItemRemoved(oldItem);
defaultStateDescriptionFragmentProvider.onItemAdded(item);
}

@Override
Expand Down
Expand Up @@ -159,6 +159,20 @@ public void testOptionsWhenTwoDescriptionProvidersHigherRankingDoesntProvideOpti
assertThat(stateDescription.getOptions(), is(stateDescriptionFragment2.getOptions()));
}

@Test
public void testPatternWhenTwoDescriptionProvidersHigherRankingDoesntProvidePattern() {
StateDescriptionFragment stateDescriptionFragment1 = StateDescriptionFragmentBuilder.create().build();
registerStateDescriptionFragmentProvider(stateDescriptionFragment1, -1);

StateDescriptionFragment stateDescriptionFragment2 = StateDescriptionFragmentBuilder.create()
.withPattern("pattern").build();
registerStateDescriptionFragmentProvider(stateDescriptionFragment2, -2);

StateDescription stateDescription = Objects.requireNonNull(item.getStateDescription());

assertThat(stateDescription.getPattern(), is(stateDescriptionFragment2.getPattern()));
}

@Test
public void testFragmentsAreMergedInProviderOrder() {
final List<StateOption> options = List.of(new StateOption("value", "label"));
Expand Down
Expand Up @@ -35,6 +35,7 @@
import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.internal.items.DefaultStateDescriptionFragmentProvider;
import org.openhab.core.internal.items.ItemBuilderFactoryImpl;
import org.openhab.core.internal.items.ItemRegistryImpl;
import org.openhab.core.items.events.ItemAddedEvent;
Expand Down Expand Up @@ -101,7 +102,8 @@ public void beforeEach() {
itemProvider.add(cameraItem4);

// setup ItemRegistryImpl with necessary dependencies:
itemRegistry = new ItemRegistryImpl(mock(MetadataRegistry.class)) {
itemRegistry = new ItemRegistryImpl(mock(MetadataRegistry.class),
mock(DefaultStateDescriptionFragmentProvider.class)) {
{
addProvider(itemProvider);
setManagedProvider(itemProvider);
Expand Down

0 comments on commit 1fb949b

Please sign in to comment.