mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-10 13:21:53 +01:00
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:
parent
49cfb20fb5
commit
97cedc46e9
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user