SseItemStatesEvent displayState: priority to transform over options (#4193)

Fix #2283
Fix #4050

This PR is fixing two things in the computation of the displayState:
- If a transformation is present in the state pattern, it has now priority over options provided by the state description
- If no transformation is present in state pattern but options are provided by the state description, the provided state pattern is applied to the matching option

Also remove a deprecated call to transform.

If there is no transformation but options are provided without any matching with the current state and there is a pattern provided, then this pattern is used to format the output.

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
lolodomo 2024-05-04 18:38:48 +02:00 committed by GitHub
parent 49cfb20fb5
commit 97cedc46e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 303 additions and 58 deletions

View File

@ -14,6 +14,7 @@ package org.openhab.core.io.rest.sse.internal;
import java.time.DateTimeException;
import java.util.HashMap;
import java.util.IllegalFormatException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@ -41,7 +42,6 @@ import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.openhab.core.types.util.UnitUtils;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@ -61,15 +61,13 @@ public class SseItemStatesEventBuilder {
private final Logger logger = LoggerFactory.getLogger(SseItemStatesEventBuilder.class);
private final BundleContext bundleContext;
private final ItemRegistry itemRegistry;
private final LocaleService localeService;
private final StartLevelService startLevelService;
@Activate
public SseItemStatesEventBuilder(final BundleContext bundleContext, final @Reference ItemRegistry itemRegistry,
public SseItemStatesEventBuilder(final @Reference ItemRegistry itemRegistry,
final @Reference LocaleService localeService, final @Reference StartLevelService startLevelService) {
this.bundleContext = bundleContext;
this.itemRegistry = itemRegistry;
this.localeService = localeService;
this.startLevelService = startLevelService;
@ -110,71 +108,78 @@ public class SseItemStatesEventBuilder {
return null;
}
private @Nullable String getDisplayState(Item item, Locale locale) {
protected @Nullable String getDisplayState(Item item, Locale locale) {
StateDescription stateDescription = item.getStateDescription(locale);
State state = item.getState();
String displayState = state.toString();
// NULL/UNDEF state is returned as "NULL"/"UNDEF" without considering anything else
if (!(state instanceof UnDefType)) {
if (stateDescription != null) {
if (!stateDescription.getOptions().isEmpty()) {
// Look for a state option with a label corresponding to the state
boolean transformUsed = false;
boolean optionMatched = false;
String pattern = stateDescription.getPattern();
// If there's a pattern, first check if it's a transformation
if (pattern != null && TransformationHelper.isTransform(pattern)) {
transformUsed = true;
try {
displayState = TransformationHelper.transform(pattern, state.toString());
} catch (TransformationException e) {
logger.warn("Failed transforming the state '{}' on item '{}' with pattern '{}': {}", state,
item.getName(), pattern, e.getMessage());
}
} else if (!stateDescription.getOptions().isEmpty()) {
// Look for a state option with a value corresponding to the state
for (StateOption option : stateDescription.getOptions()) {
if (option.getValue().equals(state.toString()) && option.getLabel() != null) {
displayState = option.getLabel();
String label = option.getLabel();
if (option.getValue().equals(state.toString()) && label != null) {
optionMatched = true;
try {
displayState = pattern == null ? label : String.format(pattern, label);
} catch (IllegalFormatException e) {
logger.debug(
"Unable to format option label '{}' of item {} using format pattern '{}': {}, displaying option label",
label, item.getName(), pattern, e.getMessage());
displayState = label;
}
break;
}
}
} else {
// If there's a pattern, first check if it's a transformation
String pattern = stateDescription.getPattern();
if (pattern != null) {
if (TransformationHelper.isTransform(pattern)) {
try {
displayState = TransformationHelper.transform(bundleContext, pattern, state.toString());
} catch (NoClassDefFoundError ex) {
// TransformationHelper is optional dependency, so ignore if class not found
// return state as it is without transformation
} catch (TransformationException e) {
logger.warn("Failed transforming the state '{}' on item '{}' with pattern '{}': {}",
state, item.getName(), pattern, e.getMessage());
}
} else {
// if it's not a transformation pattern, then it must be a format string
if (state instanceof QuantityType quantityState) {
// sanity convert current state to the item state description unit in case it was
// updated in the meantime. The item state is still in the "original" unit while the
// state description will display the new unit:
Unit<?> patternUnit = UnitUtils.parseUnit(pattern);
if (patternUnit != null && !quantityState.getUnit().equals(patternUnit)) {
quantityState = quantityState.toInvertibleUnit(patternUnit);
}
if (quantityState != null) {
state = quantityState;
}
} else if (state instanceof DateTimeType type) {
// Translate a DateTimeType state to the local time zone
try {
state = type.toLocaleZone();
} catch (DateTimeException e) {
}
}
// The following exception handling has been added to work around a Java bug with formatting
// numbers. See http://bugs.sun.com/view_bug.do?bug_id=6476425
// This also handles IllegalFormatConversionException, which is a subclass of
// IllegalArgument.
try {
displayState = state.format(pattern);
} catch (IllegalArgumentException e) {
logger.debug(
"Unable to format value '{}' of item {} with format '{}': {}, displaying raw state",
state, item.getName(), pattern, e.getMessage());
displayState = state.toString();
}
}
if (pattern != null && !transformUsed && !optionMatched) {
// if it's not a transformation pattern and there is no matching state option,
// then it must be a format string
if (state instanceof QuantityType quantityState) {
// sanity convert current state to the item state description unit in case it was
// updated in the meantime. The item state is still in the "original" unit while the
// state description will display the new unit:
Unit<?> patternUnit = UnitUtils.parseUnit(pattern);
if (patternUnit != null && !quantityState.getUnit().equals(patternUnit)) {
quantityState = quantityState.toInvertibleUnit(patternUnit);
}
if (quantityState != null) {
state = quantityState;
}
} else if (state instanceof DateTimeType type) {
// Translate a DateTimeType state to the local time zone
try {
state = type.toLocaleZone();
} catch (DateTimeException e) {
}
}
// The following exception handling has been added to work around a Java bug with formatting
// numbers. See http://bugs.sun.com/view_bug.do?bug_id=6476425
// This also handles IllegalFormatConversionException, which is a subclass of
// IllegalArgument.
try {
displayState = state.format(pattern);
} catch (IllegalArgumentException e) {
logger.debug(
"Unable to format value '{}' of item {} using format pattern '{}': {}, displaying raw state",
state, item.getName(), pattern, e.getMessage());
displayState = state.toString();
}
}
}

View File

@ -0,0 +1,240 @@
/**
* 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.io.rest.sse.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.*;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.types.StringType;
import org.openhab.core.service.StartLevelService;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
/**
* The {@link SseItemStatesEventBuilderTest} contains tests for the method getDisplayState from
* {@link SseItemStatesEventBuilder}
*
* @author Laurent Garnier - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class SseItemStatesEventBuilderTest {
private static final String ITEM_NAME = "test";
private static final String ITEM_STATE_VALUE = "value";
private static final String ITEM_STATE_VALUE2 = "other";
private static final String ITEM_STATE_OPTION_LABEL = "The value";
private static final String PATTERN = "__ %s __";
private static final String WRONG_PATTERN = "__ %d __";
private static final String TRANSFORM_NAME = "TRANSFORM";
private static final String TRANSFORM_PATTERN = "Pattern";
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_RESULT = "Result";
private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock;
private @Mock @NonNullByDefault({}) LocaleService localeServiceMock;
private @Mock @NonNullByDefault({}) StartLevelService startLevelServiceMock;
private @Mock @NonNullByDefault({}) Item itemMock;
private @Mock @NonNullByDefault({}) TransformationService transformationServiceMock;
private @Mock @NonNullByDefault({}) BundleContext bundleContextMock;
private @Mock @NonNullByDefault({}) ServiceReference<TransformationService> serviceRefMock;
private @NonNullByDefault({}) TransformationHelper transformationHelper;
private @NonNullByDefault({}) SseItemStatesEventBuilder sseItemStatesEventBuilder;
@BeforeEach
public void init() throws TransformationException {
Mockito.when(transformationServiceMock.transform(eq(TRANSFORM_PATTERN), eq(TRANSFORM_INPUT)))
.thenAnswer(answer -> TRANSFORM_RESULT);
Mockito.when(serviceRefMock.getProperty(any())).thenReturn(TRANSFORM_NAME);
Mockito.when(bundleContextMock.getService(serviceRefMock)).thenReturn(transformationServiceMock);
transformationHelper = new TransformationHelper(bundleContextMock);
transformationHelper.setTransformationService(serviceRefMock);
Mockito.when(itemMock.getName()).thenReturn(ITEM_NAME);
sseItemStatesEventBuilder = new SseItemStatesEventBuilder(itemRegistryMock, localeServiceMock,
startLevelServiceMock);
}
@AfterEach
public void tearDown() {
transformationHelper.deactivate();
}
@Test
public void getDisplayStateWhenMatchingStateOptionAndNoPattern() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
.withOption(new StateOption(ITEM_STATE_VALUE, ITEM_STATE_OPTION_LABEL)).build().toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(ITEM_STATE_OPTION_LABEL, result);
}
@Test
public void getDisplayStateWhenNoMatchingStateOptionAndNoPattern() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
.withOption(new StateOption(ITEM_STATE_VALUE, ITEM_STATE_OPTION_LABEL)).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
public void getDisplayStateWhenMatchingStateOptionAndPattern() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN)
.withOption(new StateOption(ITEM_STATE_VALUE, ITEM_STATE_OPTION_LABEL)).build().toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(String.format(PATTERN, ITEM_STATE_OPTION_LABEL), result);
}
@Test
public void getDisplayStateWhenMatchingStateOptionAndWrongPattern() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(WRONG_PATTERN)
.withOption(new StateOption(ITEM_STATE_VALUE, ITEM_STATE_OPTION_LABEL)).build().toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(ITEM_STATE_OPTION_LABEL, result);
}
@Test
public void getDisplayStateWhenNoMatchingStateOptionAndPattern() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN)
.withOption(new StateOption(ITEM_STATE_VALUE, ITEM_STATE_OPTION_LABEL)).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(String.format(PATTERN, ITEM_STATE_VALUE2), result);
}
@Test
public void getDisplayStateWhenTransformAndNoStateOption() {
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_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(TRANSFORM_RESULT, result);
}
@Test
public void getDisplayStateWhenTransformAndMatchingStateOption() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
.withPattern(TRANSFORM_NAME + "(" + TRANSFORM_PATTERN + "):" + TRANSFORM_FORMAT)
.withOption(new StateOption(ITEM_STATE_VALUE, ITEM_STATE_OPTION_LABEL)).build().toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(TRANSFORM_RESULT, result);
}
@Test
public void getDisplayStateWhenStateUndef() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN)
.withOption(new StateOption(ITEM_STATE_VALUE, ITEM_STATE_OPTION_LABEL)).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("UNDEF", result);
}
@Test
public void getDisplayStateWhenStateNull() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN)
.withOption(new StateOption(ITEM_STATE_VALUE, ITEM_STATE_OPTION_LABEL)).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("NULL", result);
}
@Test
public void getDisplayStateWhenPatternProvided() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(PATTERN).build()
.toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(String.format(PATTERN, ITEM_STATE_VALUE), result);
}
@Test
public void getDisplayStateWhenWrongPatternProvided() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().withPattern(WRONG_PATTERN).build()
.toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(ITEM_STATE_VALUE, result);
}
@Test
public void getDisplayStateWhenNoPatternProvided() {
StateDescription stateDescription = StateDescriptionFragmentBuilder.create().build().toStateDescription();
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(stateDescription);
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(ITEM_STATE_VALUE, result);
}
@Test
public void getDisplayStateWhenNoStateDescription() {
Mockito.when(itemMock.getStateDescription(eq(Locale.ENGLISH))).thenReturn(null);
Mockito.when(itemMock.getState()).thenReturn(new StringType(ITEM_STATE_VALUE));
String result = sseItemStatesEventBuilder.getDisplayState(itemMock, Locale.ENGLISH);
assertEquals(ITEM_STATE_VALUE, result);
}
}