Set a default presentation pattern for String/Number/Datetime items (#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>
This commit is contained in:
lolodomo 2024-05-02 16:29:35 +02:00 committed by GitHub
parent 1b503afdbb
commit 1fb949ba8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 142 additions and 19 deletions

View File

@ -21,7 +21,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingRegistry; import org.openhab.core.thing.ThingRegistry;
@ -100,22 +99,6 @@ public class ChannelStateDescriptionProvider implements StateDescriptionFragment
ChannelType channelType = thingTypeRegistry.getChannelType(channel, locale); ChannelType channelType = thingTypeRegistry.getChannelType(channel, locale);
if (channelType != null) { if (channelType != null) {
stateDescription = channelType.getState(); 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, StateDescription dynamicStateDescription = getDynamicStateDescription(channel, stateDescription,
locale); locale);

View File

@ -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;
}
}

View File

@ -59,6 +59,7 @@ import org.slf4j.LoggerFactory;
* *
* @author Kai Kreuzer - Initial contribution * @author Kai Kreuzer - Initial contribution
* @author Stefan Bußweiler - Migration to new event mechanism * @author Stefan Bußweiler - Migration to new event mechanism
* @author Laurent Garnier - handle new DefaultStateDescriptionFragmentProvider
*/ */
@NonNullByDefault @NonNullByDefault
@Component(immediate = true) @Component(immediate = true)
@ -71,13 +72,16 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
private @Nullable StateDescriptionService stateDescriptionService; private @Nullable StateDescriptionService stateDescriptionService;
private @Nullable CommandDescriptionService commandDescriptionService; private @Nullable CommandDescriptionService commandDescriptionService;
private final MetadataRegistry metadataRegistry; private final MetadataRegistry metadataRegistry;
private final DefaultStateDescriptionFragmentProvider defaultStateDescriptionFragmentProvider;
private @Nullable ItemStateConverter itemStateConverter; private @Nullable ItemStateConverter itemStateConverter;
@Activate @Activate
public ItemRegistryImpl(final @Reference MetadataRegistry metadataRegistry) { public ItemRegistryImpl(final @Reference MetadataRegistry metadataRegistry,
final @Reference DefaultStateDescriptionFragmentProvider defaultStateDescriptionFragmentProvider) {
super(ItemProvider.class); super(ItemProvider.class);
this.metadataRegistry = metadataRegistry; this.metadataRegistry = metadataRegistry;
this.defaultStateDescriptionFragmentProvider = defaultStateDescriptionFragmentProvider;
} }
@Activate @Activate
@ -198,6 +202,8 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
// add the item to all relevant groups // add the item to all relevant groups
addToGroupItems(item, item.getGroupNames()); addToGroupItems(item, item.getGroupNames());
defaultStateDescriptionFragmentProvider.onItemAdded(item);
} }
private void injectServices(Item item) { private void injectServices(Item item) {
@ -246,6 +252,7 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
genericItem.dispose(); genericItem.dispose();
} }
removeFromGroupItems(element, element.getGroupNames()); removeFromGroupItems(element, element.getGroupNames());
defaultStateDescriptionFragmentProvider.onItemRemoved(element);
} }
@Override @Override
@ -269,6 +276,9 @@ public class ItemRegistryImpl extends AbstractRegistry<Item, String, ItemProvide
addMembersToGroupItem(groupItem); addMembersToGroupItem(groupItem);
} }
injectServices(item); injectServices(item);
defaultStateDescriptionFragmentProvider.onItemRemoved(oldItem);
defaultStateDescriptionFragmentProvider.onItemAdded(item);
} }
@Override @Override

View File

@ -159,6 +159,20 @@ public class StateDescriptionServiceImplTest {
assertThat(stateDescription.getOptions(), is(stateDescriptionFragment2.getOptions())); 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 @Test
public void testFragmentsAreMergedInProviderOrder() { public void testFragmentsAreMergedInProviderOrder() {
final List<StateOption> options = List.of(new StateOption("value", "label")); final List<StateOption> options = List.of(new StateOption("value", "label"));

View File

@ -35,6 +35,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.UnitProvider; 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.ItemBuilderFactoryImpl;
import org.openhab.core.internal.items.ItemRegistryImpl; import org.openhab.core.internal.items.ItemRegistryImpl;
import org.openhab.core.items.events.ItemAddedEvent; import org.openhab.core.items.events.ItemAddedEvent;
@ -101,7 +102,8 @@ public class ItemRegistryImplTest extends JavaTest {
itemProvider.add(cameraItem4); itemProvider.add(cameraItem4);
// setup ItemRegistryImpl with necessary dependencies: // setup ItemRegistryImpl with necessary dependencies:
itemRegistry = new ItemRegistryImpl(mock(MetadataRegistry.class)) { itemRegistry = new ItemRegistryImpl(mock(MetadataRegistry.class),
mock(DefaultStateDescriptionFragmentProvider.class)) {
{ {
addProvider(itemProvider); addProvider(itemProvider);
setManagedProvider(itemProvider); setManagedProvider(itemProvider);