Make input formatting for transformations consistent everywhere (#4203)

Use item state formatter to format input of transformation, meaning using state.format(format) instead of String.format(format, state.toString())
This was already the case in sitemap API but not in other APIs used by Main UI.

Make sure to call transformation even for NULL and UNDEF states.
It was not the case in one API used by Main UI.

When calling transformation and state is NULL or UNDEF, do not apply format to the input value and do not replace by "-".
That means the transformation will be called with "NULL" or "UNDEF".
Sitemap API was calling the transformation using a pattern containing "-".

Fix #4101
Also related to discussion in openhab/openhab-addons#13777

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
lolodomo 2024-06-26 21:25:13 +02:00 committed by GitHub
parent cb65e41445
commit d22e14a1e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 246 additions and 68 deletions

View File

@ -18,6 +18,8 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
@ -30,9 +32,11 @@ import org.openhab.core.items.dto.ItemDTOMapper;
import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.items.NumberItem;
import org.openhab.core.transform.TransformationException; import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationHelper; import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.State;
import org.openhab.core.types.StateDescription; import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragmentBuilder; import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.osgi.framework.FrameworkUtil; import org.openhab.core.types.UnDefType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -45,6 +49,8 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
public class EnrichedItemDTOMapper { public class EnrichedItemDTOMapper {
private static final Pattern EXTRACT_TRANSFORM_FUNCTION_PATTERN = Pattern.compile("(.*?)\\((.*)\\):(.*)");
private static final Logger LOGGER = LoggerFactory.getLogger(EnrichedItemDTOMapper.class); private static final Logger LOGGER = LoggerFactory.getLogger(EnrichedItemDTOMapper.class);
/** /**
@ -79,8 +85,8 @@ public class EnrichedItemDTOMapper {
parents.add(item); parents.add(item);
} }
String state = item.getState().toFullString(); String state = item.getState().toFullString();
String transformedState = considerTransformation(state, item, locale); String transformedState = considerTransformation(item, locale);
if (transformedState != null && transformedState.equals(state)) { if (state.equals(transformedState)) {
transformedState = null; transformedState = null;
} }
StateDescription stateDescription = considerTransformation(item.getStateDescription(locale)); StateDescription stateDescription = considerTransformation(item.getStateDescription(locale));
@ -132,37 +138,48 @@ public class EnrichedItemDTOMapper {
if (stateDescription != null) { if (stateDescription != null) {
String pattern = stateDescription.getPattern(); String pattern = stateDescription.getPattern();
if (pattern != null) { if (pattern != null) {
try { return TransformationHelper.isTransform(pattern)
return TransformationHelper.isTransform(pattern) ? StateDescriptionFragmentBuilder.create(stateDescription).withPattern(pattern).build()
? StateDescriptionFragmentBuilder.create(stateDescription).withPattern(pattern).build() .toStateDescription()
.toStateDescription() : stateDescription;
: stateDescription;
} catch (NoClassDefFoundError ex) {
// TransformationHelper is optional dependency, so ignore if class not found
// return state description as it is without transformation
}
} }
} }
return stateDescription; return stateDescription;
} }
private static @Nullable String considerTransformation(String state, Item item, @Nullable Locale locale) { private static @Nullable String considerTransformation(Item item, @Nullable Locale locale) {
StateDescription stateDescription = item.getStateDescription(locale); StateDescription stateDescription = item.getStateDescription(locale);
if (stateDescription != null) { if (stateDescription != null) {
String pattern = stateDescription.getPattern(); String pattern = stateDescription.getPattern();
if (pattern != null) { Matcher matcher;
if (pattern != null && (matcher = EXTRACT_TRANSFORM_FUNCTION_PATTERN.matcher(pattern)).find()) {
State state = item.getState();
try { try {
return TransformationHelper.transform( String type = matcher.group(1);
FrameworkUtil.getBundle(EnrichedItemDTOMapper.class).getBundleContext(), pattern, state); String function = matcher.group(2);
} catch (NoClassDefFoundError ex) { String value = matcher.group(3);
// TransformationHelper is optional dependency, so ignore if class not found TransformationService transformation = TransformationHelper.getTransformationService(type);
// return state as it is without transformation if (transformation != null) {
String format = state instanceof UnDefType ? "%s" : value;
try {
return transformation.transform(function, state.format(format));
} catch (IllegalArgumentException e) {
throw new TransformationException(
"Cannot format state '" + state + "' to format '" + format + "'", e);
} catch (RuntimeException e) {
throw new TransformationException("Transformation service of type '" + type
+ "' threw an exception: " + e.getMessage(), e);
}
} else {
throw new TransformationException(
"Transformation service of type '" + type + "' is not available.");
}
} catch (TransformationException e) { } catch (TransformationException e) {
LOGGER.warn("Failed transforming the state '{}' on item '{}' with pattern '{}': {}", state, LOGGER.warn("Failed transforming the state '{}' on item '{}' with pattern '{}': {}", state,
item.getName(), pattern, e.getMessage()); item.getName(), pattern, e.getMessage());
} }
} }
} }
return state; return null;
} }
} }

View File

@ -18,6 +18,8 @@ import java.util.IllegalFormatException;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.measure.Unit; import javax.measure.Unit;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
@ -37,6 +39,7 @@ import org.openhab.core.library.types.QuantityType;
import org.openhab.core.service.StartLevelService; import org.openhab.core.service.StartLevelService;
import org.openhab.core.transform.TransformationException; import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationHelper; import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.StateDescription; import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateOption; import org.openhab.core.types.StateOption;
@ -59,6 +62,8 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
public class SseItemStatesEventBuilder { public class SseItemStatesEventBuilder {
private static final Pattern EXTRACT_TRANSFORM_FUNCTION_PATTERN = Pattern.compile("(.*?)\\((.*)\\):(.*)");
private final Logger logger = LoggerFactory.getLogger(SseItemStatesEventBuilder.class); private final Logger logger = LoggerFactory.getLogger(SseItemStatesEventBuilder.class);
private final ItemRegistry itemRegistry; private final ItemRegistry itemRegistry;
@ -113,22 +118,43 @@ public class SseItemStatesEventBuilder {
State state = item.getState(); State state = item.getState();
String displayState = state.toString(); String displayState = state.toString();
// NULL/UNDEF state is returned as "NULL"/"UNDEF" without considering anything else if (stateDescription != null) {
if (!(state instanceof UnDefType)) { String pattern = stateDescription.getPattern();
if (stateDescription != null) { // First check if the pattern is a transformation
boolean transformUsed = false; Matcher matcher;
boolean optionMatched = false; if (pattern != null && (matcher = EXTRACT_TRANSFORM_FUNCTION_PATTERN.matcher(pattern)).find()) {
String pattern = stateDescription.getPattern(); try {
// If there's a pattern, first check if it's a transformation String type = matcher.group(1);
if (pattern != null && TransformationHelper.isTransform(pattern)) { String function = matcher.group(2);
transformUsed = true; String value = matcher.group(3);
try { TransformationService transformation = TransformationHelper.getTransformationService(type);
displayState = TransformationHelper.transform(pattern, state.toString()); if (transformation != null) {
} catch (TransformationException e) { String format = state instanceof UnDefType ? "%s" : value;
logger.warn("Failed transforming the state '{}' on item '{}' with pattern '{}': {}", state, try {
item.getName(), pattern, e.getMessage()); displayState = transformation.transform(function, state.format(format));
if (displayState == null) {
displayState = state.toString();
}
} catch (IllegalArgumentException e) {
throw new TransformationException(
"Cannot format state '" + state + "' to format '" + format + "'", e);
} catch (RuntimeException e) {
throw new TransformationException("Transformation service of type '" + type
+ "' threw an exception: " + e.getMessage(), e);
}
} else {
throw new TransformationException(
"Transformation service of type '" + type + "' is not available.");
} }
} else if (!stateDescription.getOptions().isEmpty()) { } catch (TransformationException e) {
logger.warn("Failed transforming the state '{}' on item '{}' with pattern '{}': {}", state,
item.getName(), pattern, e.getMessage());
}
}
// If no transformation, NULL/UNDEF state is returned as "NULL"/"UNDEF" without considering anything else
else if (!(state instanceof UnDefType)) {
boolean optionMatched = false;
if (!stateDescription.getOptions().isEmpty()) {
// Look for a state option with a value corresponding to the state // Look for a state option with a value corresponding to the state
for (StateOption option : stateDescription.getOptions()) { for (StateOption option : stateDescription.getOptions()) {
String label = option.getLabel(); String label = option.getLabel();
@ -146,7 +172,7 @@ public class SseItemStatesEventBuilder {
} }
} }
} }
if (pattern != null && !transformUsed && !optionMatched) { if (pattern != null && !optionMatched) {
// if it's not a transformation pattern and there is no matching state option, // if it's not a transformation pattern and there is no matching state option,
// then it must be a format string // then it must be a format string
if (state instanceof QuantityType quantityState) { if (state instanceof QuantityType quantityState) {

View File

@ -30,6 +30,7 @@ import org.mockito.quality.Strictness;
import org.openhab.core.io.rest.LocaleService; import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.service.StartLevelService; import org.openhab.core.service.StartLevelService;
import org.openhab.core.transform.TransformationException; import org.openhab.core.transform.TransformationException;
@ -55,16 +56,23 @@ public class SseItemStatesEventBuilderTest {
private static final String ITEM_NAME = "test"; private static final String ITEM_NAME = "test";
private static final String ITEM_STATE_VALUE = "value"; private static final String ITEM_STATE_VALUE = "value";
private static final String ITEM_STATE_VALUE2 = "other"; private static final String ITEM_STATE_VALUE2 = "other";
private static final int ITEM_STATE_VALUE3 = 16;
private static final String ITEM_STATE_OPTION_LABEL = "The value"; private static final String ITEM_STATE_OPTION_LABEL = "The value";
private static final String PATTERN = "__ %s __"; private static final String PATTERN = "__ %s __";
private static final String WRONG_PATTERN = "__ %d __"; private static final String PATTERN2 = "__ %d __";
private static final String TRANSFORM_NAME = "TRANSFORM"; private static final String TRANSFORM_NAME = "TRANSFORM";
private static final String TRANSFORM_PATTERN = "Pattern"; private static final String TRANSFORM_PATTERN = "Pattern";
private static final String TRANSFORM_FORMAT = "%s-1"; private static final String TRANSFORM_FORMAT = "%s-1";
private static final String TRANSFORM_INPUT = String.format(TRANSFORM_FORMAT, ITEM_STATE_VALUE); private static final String TRANSFORM_INPUT = String.format(TRANSFORM_FORMAT, ITEM_STATE_VALUE);
private static final String TRANSFORM_RESULT = "Result"; private static final String TRANSFORM_INPUT2 = String.format(TRANSFORM_FORMAT, ITEM_STATE_VALUE2);
private static final String TRANSFORM_RESULT = "Result with string";
private static final String TRANSFORM_FORMAT_NUMBER = "_%d_";
private static final String TRANSFORM_INPUT3 = String.format(TRANSFORM_FORMAT_NUMBER, ITEM_STATE_VALUE3);
private static final String TRANSFORM_RESULT_NUMBER = "Result with number";
private static final String TRANSFORM_RESULT_NULL = "State is NULL";
private static final String TRANSFORM_RESULT_UNDEF = "State is UNDEF";
private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock; private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock;
private @Mock @NonNullByDefault({}) LocaleService localeServiceMock; private @Mock @NonNullByDefault({}) LocaleService localeServiceMock;
@ -85,6 +93,14 @@ public class SseItemStatesEventBuilderTest {
public void init() throws TransformationException { public void init() throws TransformationException {
Mockito.when(transformationServiceMock.transform(eq(TRANSFORM_PATTERN), eq(TRANSFORM_INPUT))) Mockito.when(transformationServiceMock.transform(eq(TRANSFORM_PATTERN), eq(TRANSFORM_INPUT)))
.thenAnswer(answer -> TRANSFORM_RESULT); .thenAnswer(answer -> TRANSFORM_RESULT);
Mockito.when(transformationServiceMock.transform(eq(TRANSFORM_PATTERN), eq(TRANSFORM_INPUT2)))
.thenAnswer(answer -> null);
Mockito.when(transformationServiceMock.transform(eq(TRANSFORM_PATTERN), eq(TRANSFORM_INPUT3)))
.thenAnswer(answer -> TRANSFORM_RESULT_NUMBER);
Mockito.when(transformationServiceMock.transform(eq(TRANSFORM_PATTERN), eq("NULL")))
.thenAnswer(answer -> TRANSFORM_RESULT_NULL);
Mockito.when(transformationServiceMock.transform(eq(TRANSFORM_PATTERN), eq("UNDEF")))
.thenAnswer(answer -> TRANSFORM_RESULT_UNDEF);
Mockito.when(serviceRefMock.getProperty(any())).thenReturn(TRANSFORM_NAME); Mockito.when(serviceRefMock.getProperty(any())).thenReturn(TRANSFORM_NAME);
@ -139,7 +155,7 @@ public class SseItemStatesEventBuilderTest {
@Test @Test
public void getDisplayStateWhenMatchingStateOptionAndWrongPattern() { public void getDisplayStateWhenMatchingStateOptionAndWrongPattern() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(WRONG_PATTERN) StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN2)
.withOption(new StateOption(ITEM_STATE_VALUE, ITEM_STATE_OPTION_LABEL)).build().toStateDescription(); .withOption(new StateOption(ITEM_STATE_VALUE, ITEM_STATE_OPTION_LABEL)).build().toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription); Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
@ -168,6 +184,14 @@ public class SseItemStatesEventBuilderTest {
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE)); Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH); String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(TRANSFORM_RESULT, result); assertEquals(TRANSFORM_RESULT, result);
StateDescription stateDescription2 = StateDescriptionFragmentBuilder.create()
.withPattern(TRANSFORM_NAME + "(" + TRANSFORM_PATTERN + "):" + TRANSFORM_FORMAT_NUMBER).build()
.toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription2);
Mockito.when(itemMock.getState()).thenReturn(new DecimalType(ITEM_STATE_VALUE3));
result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(TRANSFORM_RESULT_NUMBER, result);
} }
@Test @Test
@ -179,6 +203,25 @@ public class SseItemStatesEventBuilderTest {
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE)); Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH); String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(TRANSFORM_RESULT, result); assertEquals(TRANSFORM_RESULT, result);
StateDescription stateDescription2 = StateDescriptionFragmentBuilder.create()
.withPattern(TRANSFORM_NAME + "(" + TRANSFORM_PATTERN + "):" + TRANSFORM_FORMAT_NUMBER).build()
.toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription2);
Mockito.when(itemMock.getState()).thenReturn(new DecimalType(ITEM_STATE_VALUE3));
result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(TRANSFORM_RESULT_NUMBER, result);
}
@Test
public void getDisplayStateWhenTransformReturningNull() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
.withPattern(TRANSFORM_NAME + "(" + TRANSFORM_PATTERN + "):" + TRANSFORM_FORMAT).build()
.toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE2));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(ITEM_STATE_VALUE2, result);
} }
@Test @Test
@ -201,6 +244,28 @@ public class SseItemStatesEventBuilderTest {
assertEquals("NULL", result); assertEquals("NULL", result);
} }
@Test
public void getDisplayStateWhenTransformAndStateUndef() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN)
.withPattern(TRANSFORM_NAME + "(" + TRANSFORM_PATTERN + "):" + TRANSFORM_FORMAT_NUMBER).build()
.toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(UnDefType.UNDEF);
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(TRANSFORM_RESULT_UNDEF, result);
}
@Test
public void getDisplayStateWhenTransformAndStateNull() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN)
.withPattern(TRANSFORM_NAME + "(" + TRANSFORM_PATTERN + "):" + TRANSFORM_FORMAT_NUMBER).build()
.toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(UnDefType.NULL);
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(TRANSFORM_RESULT_NULL, result);
}
@Test @Test
public void getDisplayStateWhenPatternProvided() { public void getDisplayStateWhenPatternProvided() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN).build() StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN).build()
@ -209,11 +274,18 @@ public class SseItemStatesEventBuilderTest {
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE)); Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH); String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(String.format(PATTERN, ITEM_STATE_VALUE), result); assertEquals(String.format(PATTERN, ITEM_STATE_VALUE), result);
StateDescription stateDescription2 = StateDescriptionFragmentBuilder.create().withPattern(PATTERN2).build()
.toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription2);
Mockito.when(itemMock.getState()).thenReturn(new DecimalType(ITEM_STATE_VALUE3));
result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(String.format(PATTERN2, ITEM_STATE_VALUE3), result);
} }
@Test @Test
public void getDisplayStateWhenWrongPatternProvided() { public void getDisplayStateWhenWrongPatternProvided() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(WRONG_PATTERN).build() StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN2).build()
.toStateDescription(); .toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription); Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE)); Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
@ -228,6 +300,10 @@ public class SseItemStatesEventBuilderTest {
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE)); Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH); String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(ITEM_STATE_VALUE, result); assertEquals(ITEM_STATE_VALUE, result);
Mockito.when(itemMock.getState()).thenReturn(new DecimalType(ITEM_STATE_VALUE3));
result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(String.format("%d", ITEM_STATE_VALUE3), result);
} }
@Test @Test
@ -236,5 +312,9 @@ public class SseItemStatesEventBuilderTest {
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE)); Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH); String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(ITEM_STATE_VALUE, result); assertEquals(ITEM_STATE_VALUE, result);
Mockito.when(itemMock.getState()).thenReturn(new DecimalType(ITEM_STATE_VALUE3));
result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(String.format("%d", ITEM_STATE_VALUE3), result);
} }
} }

View File

@ -86,7 +86,6 @@ import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateOption; import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import org.openhab.core.types.util.UnitUtils; import org.openhab.core.types.util.UnitUtils;
import org.openhab.core.ui.internal.UIActivator;
import org.openhab.core.ui.items.ItemUIProvider; import org.openhab.core.ui.items.ItemUIProvider;
import org.openhab.core.ui.items.ItemUIRegistry; import org.openhab.core.ui.items.ItemUIRegistry;
import org.osgi.framework.Constants; import org.osgi.framework.Constants;
@ -341,7 +340,7 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
String itemName = w.getItem(); String itemName = w.getItem();
if (itemName == null || itemName.isBlank()) { if (itemName == null || itemName.isBlank()) {
return transform(label, true, null); return transform(label, true, null, null);
} }
String labelMappedOption = null; String labelMappedOption = null;
@ -373,7 +372,7 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
state = item.getState(); state = item.getState();
if (formatPattern.contains("%d")) { if (formatPattern.contains("%d")) {
if (!(state instanceof Number)) { if (!(state instanceof UnDefType) && !(state instanceof Number)) {
// States which do not provide a Number will be converted to DecimalType. // States which do not provide a Number will be converted to DecimalType.
// e.g.: GroupItem can provide a count of items matching the active state // e.g.: GroupItem can provide a count of items matching the active state
// for some group functions. // for some group functions.
@ -390,13 +389,25 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
} }
boolean considerTransform = false; boolean considerTransform = false;
String transformFailbackValue = null;
if (formatPattern != null) { if (formatPattern != null) {
if (formatPattern.isEmpty()) { if (formatPattern.isEmpty()) {
label = label.substring(0, label.indexOf("[")).trim(); label = label.substring(0, label.indexOf("[")).trim();
} else { } else {
if (state == null || state instanceof UnDefType) { if (state == null) {
formatPattern = formatUndefined(formatPattern); formatPattern = formatUndefined(formatPattern);
considerTransform = true; considerTransform = true;
} else if (state instanceof UnDefType) {
Matcher matcher = EXTRACT_TRANSFORM_FUNCTION_PATTERN.matcher(formatPattern);
if (matcher.find()) {
considerTransform = true;
String type = matcher.group(1);
String function = matcher.group(2);
formatPattern = type + "(" + function + "):" + state.toString();
transformFailbackValue = "-";
} else {
formatPattern = formatUndefined(formatPattern);
}
} else { } else {
// if the channel contains options, we build a label with the mapped option value // if the channel contains options, we build a label with the mapped option value
if (stateDescription != null) { if (stateDescription != null) {
@ -457,9 +468,10 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
if (matcher.find()) { if (matcher.find()) {
considerTransform = true; considerTransform = true;
String type = matcher.group(1); String type = matcher.group(1);
String pattern = matcher.group(2); String function = matcher.group(2);
String value = matcher.group(3); String value = matcher.group(3);
formatPattern = type + "(" + pattern + "):" + state.format(value); formatPattern = type + "(" + function + "):" + state.format(value);
transformFailbackValue = state.toString();
} else { } else {
formatPattern = state.format(formatPattern); formatPattern = state.format(formatPattern);
} }
@ -478,7 +490,7 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
} }
} }
return transform(label, considerTransform, labelMappedOption); return transform(label, considerTransform, transformFailbackValue, labelMappedOption);
} }
@Override @Override
@ -619,36 +631,40 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
* If the value does not start with the call to a transformation service, * If the value does not start with the call to a transformation service,
* we return the label with the mapped option value if provided (not null). * we return the label with the mapped option value if provided (not null).
*/ */
private String transform(String label, boolean matchTransform, @Nullable String labelMappedOption) { private String transform(String label, boolean matchTransform, @Nullable String transformFailbackValue,
@Nullable String labelMappedOption) {
String ret = label; String ret = label;
String formatPattern = getFormatPattern(label); String formatPattern = getFormatPattern(label);
if (formatPattern != null) { if (formatPattern != null) {
Matcher matcher = EXTRACT_TRANSFORM_FUNCTION_PATTERN.matcher(formatPattern); Matcher matcher = EXTRACT_TRANSFORM_FUNCTION_PATTERN.matcher(formatPattern);
if (matchTransform && matcher.find()) { if (matchTransform && matcher.find()) {
String type = matcher.group(1); String type = matcher.group(1);
String pattern = matcher.group(2); String function = matcher.group(2);
String value = matcher.group(3); String value = matcher.group(3);
TransformationService transformation = TransformationHelper String failbackValue = transformFailbackValue != null ? transformFailbackValue : value;
.getTransformationService(UIActivator.getContext(), type); try {
if (transformation != null) { TransformationService transformation = TransformationHelper.getTransformationService(type);
try { if (transformation != null) {
String transformationResult = transformation.transform(pattern, value); try {
if (transformationResult != null) { String transformationResult = transformation.transform(function, value);
ret = insertInLabel(label, transformationResult); if (transformationResult != null) {
} else { ret = insertInLabel(label, transformationResult);
logger.warn("transformation of type {} did not return a valid result", type); } else {
ret = insertInLabel(label, UnDefType.NULL); logger.warn("Transformation of type {} did not return a valid result", type);
ret = insertInLabel(label, failbackValue);
}
} catch (RuntimeException e) {
throw new TransformationException("Transformation service of type '" + type
+ "' threw an exception: " + e.getMessage(), e);
} }
} catch (TransformationException e) { } else {
logger.error("transformation throws exception [transformation={}, value={}]", transformation, throw new TransformationException(
value, e); "Transformation service of type '" + type + "' is not available.");
ret = insertInLabel(label, value);
} }
} else { } catch (TransformationException e) {
logger.warn( logger.warn("Failed transforming the value '{}' with pattern '{}': {}", value, formatPattern,
"couldn't transform value in label because transformationService of type '{}' is unavailable", e.getMessage());
type); ret = insertInLabel(label, failbackValue);
ret = insertInLabel(label, value);
} }
} else if (labelMappedOption != null) { } else if (labelMappedOption != null) {
ret = labelMappedOption; ret = labelMappedOption;

View File

@ -809,6 +809,45 @@ public class ItemUIRegistryImplTest {
assertEquals("Memory [State]", label); assertEquals("Memory [State]", label);
} }
@Test
public void getLabelFailingTransformation() throws ItemNotFoundException {
String testLabel = "Memory [FOO(echo %s):__%d__]";
Widget w = mock(Widget.class);
Item item = mock(Item.class);
when(w.getLabel()).thenReturn(testLabel);
when(w.getItem()).thenReturn(ITEM_NAME);
when(registryMock.getItem(ITEM_NAME)).thenReturn(item);
when(item.getState()).thenReturn(new DecimalType(11));
String label = uiRegistry.getLabel(w);
assertEquals("Memory [11]", label);
}
@Test
public void getLabelFailingTransformationWithNullState() throws ItemNotFoundException {
String testLabel = "Memory [FOO(echo %s):__%d__]";
Widget w = mock(Widget.class);
Item item = mock(Item.class);
when(w.getLabel()).thenReturn(testLabel);
when(w.getItem()).thenReturn(ITEM_NAME);
when(registryMock.getItem(ITEM_NAME)).thenReturn(item);
when(item.getState()).thenReturn(UnDefType.NULL);
String label = uiRegistry.getLabel(w);
assertEquals("Memory [-]", label);
}
@Test
public void getLabelFailingTransformationWithUndefState() throws ItemNotFoundException {
String testLabel = "Memory [FOO(echo %s):__%d__]";
Widget w = mock(Widget.class);
Item item = mock(Item.class);
when(w.getLabel()).thenReturn(testLabel);
when(w.getItem()).thenReturn(ITEM_NAME);
when(registryMock.getItem(ITEM_NAME)).thenReturn(item);
when(item.getState()).thenReturn(UnDefType.UNDEF);
String label = uiRegistry.getLabel(w);
assertEquals("Memory [-]", label);
}
@Test @Test
public void getLabelColorLabelWithDecimalValue() { public void getLabelColorLabelWithDecimalValue() {
String testLabel = "Label [%.3f]"; String testLabel = "Label [%.3f]";