From 5ceaa64768ed1f22e40d7b72c7d95aced28a5fa1 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Sun, 2 Jul 2023 11:03:00 +0200 Subject: [PATCH] [rest] Add widget state pattern and default unit to ItemUIRegistry (#3644) * expose widget format pattern to sitemap REST * Add unit fields to sitemap REST Signed-off-by: Mark Herwege --- .../sitemap/internal/SitemapResource.java | 3 + .../io/rest/sitemap/internal/WidgetDTO.java | 4 + .../ui/internal/items/ItemUIRegistryImpl.java | 109 +++++++++++++----- .../openhab/core/ui/items/ItemUIRegistry.java | 10 ++ 4 files changed, 98 insertions(+), 28 deletions(-) diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java index 478f244a141..84d3823ff44 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java @@ -125,6 +125,7 @@ * @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification * @author Wouter Born - Migrated to OpenAPI annotations * @author Laurent Garnier - Added support for icon color + * @author Mark Herwege - Added pattern and unit fields */ @Component(service = RESTResource.class) @JaxrsResource @@ -520,6 +521,8 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null bean.valuecolor = convertItemValueColor(itemUIRegistry.getValueColor(widget), itemState); bean.iconcolor = convertItemValueColor(itemUIRegistry.getIconColor(widget), itemState); bean.label = itemUIRegistry.getLabel(widget); + bean.pattern = itemUIRegistry.getFormatPattern(widget); + bean.unit = itemUIRegistry.getUnitForWidget(widget); bean.type = widget.eClass().getName(); bean.visibility = itemUIRegistry.getVisiblity(widget); if (widget instanceof LinkableWidget linkableWidget) { diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java index db47ddd27e8..c9dc713b082 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java @@ -24,6 +24,7 @@ * @author Kai Kreuzer - Initial contribution * @author Chris Jackson - Initial contribution * @author Laurent Garnier - New field iconcolor + * @author Mark herwege - New fields pattern, unit */ public class WidgetDTO { @@ -38,6 +39,9 @@ public class WidgetDTO { public String valuecolor; public String iconcolor; + public String pattern; + public String unit; + // widget-specific attributes public final List mappings = new ArrayList<>(); public Boolean switchSupport; diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java index 4be4185cddc..d5a69469427 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java @@ -108,6 +108,7 @@ * @author Stefan Triller - Method to convert a state into something a sitemap entity can understand * @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType * @author Laurent Garnier - new method getIconColor + * @author Mark Herwege - new method getFormatPattern(widget), clean pattern */ @NonNullByDefault @Component(immediate = true, configurationPid = "org.openhab.sitemap", // @@ -121,9 +122,9 @@ public class ItemUIRegistryImpl implements ItemUIRegistry { protected static final Pattern EXTRACT_TRANSFORM_FUNCTION_PATTERN = Pattern.compile("(.*?)\\((.*)\\):(.*)"); /* RegEx to identify format patterns. See java.util.Formatter#formatSpecifier (without the '%' at the very end). */ - protected static final String IDENTIFY_FORMAT_PATTERN_PATTERN = "%((unit%)|((\\d+\\$)?([-#+ 0,(<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z])))"; + protected static final String IDENTIFY_FORMAT_PATTERN_PATTERN = "%(?:(unit%)|(?:(?:\\d+\\$)?(?:[-#+ 0,(<]*)?(?:\\d+)?(?:\\.\\d+)?(?:[tT])?(?:[a-zA-Z])))"; + private static final Pattern FORMAT_PATTERN = Pattern.compile("(?:^|[^%])" + IDENTIFY_FORMAT_PATTERN_PATTERN); - private static final Pattern LABEL_PATTERN = Pattern.compile(".*?\\[.*? (.*?)]"); private static final int MAX_BUTTONS = 4; private static final String DEFAULT_SORTING = "NONE"; @@ -331,10 +332,14 @@ private Switch createPlayerButtons() { String labelMappedOption = null; State state = null; StateDescription stateDescription = null; - String formatPattern = getFormatPattern(label); + String formatPattern = getFormatPattern(w); + + if (formatPattern != null && label.indexOf("[") < 0) { + label = label + " [" + formatPattern + "]"; + } // now insert the value, if the state is a string or decimal value and there is some formatting pattern defined - // in the label (i.e. it contains at least a %) + // in the label or state description (i.e. it contains at least a %) try { final Item item = getItem(itemName); @@ -348,13 +353,8 @@ private Switch createPlayerButtons() { // returned StateDescription. What is expected is the display of a value using the pattern // provided by the channel state description provider. stateDescription = item.getStateDescription(); - if (formatPattern == null && stateDescription != null && stateDescription.getPattern() != null) { - label = label + " [" + stateDescription.getPattern() + "]"; - } - String updatedPattern = getFormatPattern(label); - if (updatedPattern != null) { - formatPattern = updatedPattern; + if (formatPattern != null) { state = item.getState(); if (formatPattern.contains("%d")) { @@ -371,7 +371,7 @@ private Switch createPlayerButtons() { } } } catch (ItemNotFoundException e) { - logger.error("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName()); + logger.warn("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName()); } boolean considerTransform = false; @@ -455,7 +455,10 @@ private Switch createPlayerButtons() { } label = label.trim(); - label = label.substring(0, label.indexOf("[") + 1) + formatPattern + "]"; + int index = label.indexOf("["); + if (index >= 0) { + label = label.substring(0, index + 1) + formatPattern + "]"; + } } } @@ -463,7 +466,7 @@ private Switch createPlayerButtons() { } private QuantityType convertStateToWidgetUnit(QuantityType quantityState, Widget w) { - Unit widgetUnit = UnitUtils.parseUnit(getFormatPattern(w.getLabel())); + Unit widgetUnit = UnitUtils.parseUnit(getFormatPattern(w)); if (widgetUnit != null && !widgetUnit.equals(quantityState.getUnit())) { return Objects.requireNonNullElse(quantityState.toInvertibleUnit(widgetUnit), quantityState); } @@ -471,6 +474,56 @@ private QuantityType convertStateToWidgetUnit(QuantityType quantityState, return quantityState; } + @Override + public @Nullable String getFormatPattern(Widget w) { + String label = getLabelFromWidget(w); + String pattern = getFormatPattern(label); + String itemName = w.getItem(); + try { + Item item = null; + if (itemName != null && !itemName.isBlank()) { + item = getItem(itemName); + } + if (item != null && pattern == null) { + StateDescription stateDescription = item.getStateDescription(); + if (stateDescription != null) { + pattern = stateDescription.getPattern(); + } + } + + if (pattern == null) { + return null; + } + + // remove last part of pattern, after unit, if it exists, as this is not valid and creates problems with + // updates + if (item instanceof NumberItem numberItem && numberItem.getDimension() != null) { + Matcher m = FORMAT_PATTERN.matcher(pattern); + int matcherEnd = 0; + if (m.find() && m.group(1) == null) { + matcherEnd = m.end(); + } + String unit = pattern.substring(matcherEnd).trim(); + String postfix = ""; + int unitEnd = unit.indexOf(" "); + if (unitEnd > -1) { + postfix = unit.substring(unitEnd + 1).trim(); + unit = unit.substring(0, unitEnd); + } + if (!postfix.isBlank()) { + logger.warn( + "Item '{}' with unit, nothing allowed after unit in label pattern '{}', dropping postfix", + itemName, pattern); + } + pattern = pattern.substring(0, matcherEnd) + (!unit.isBlank() ? " " + unit : ""); + } + } catch (ItemNotFoundException e) { + logger.warn("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName()); + } + + return pattern; + } + private @Nullable String getFormatPattern(@Nullable String label) { if (label == null) { return null; @@ -608,7 +661,7 @@ private String transform(String label, boolean matchTransform, @Nullable String Item item = getItem(itemName); return convertState(w, item, item.getState()); } catch (ItemNotFoundException e) { - logger.error("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName()); + logger.warn("Cannot retrieve item '{}' for widget {}", itemName, w.eClass().getInstanceTypeName()); } } return UnDefType.UNDEF; @@ -627,8 +680,8 @@ private String transform(String label, boolean matchTransform, @Nullable String State returnState = null; State itemState = i.getState(); - if (itemState instanceof QuantityType type) { - itemState = convertStateToWidgetUnit(type, w); + if (itemState instanceof QuantityType quantityTypeState) { + itemState = convertStateToWidgetUnit(quantityTypeState, w); } if (w instanceof Switch && i instanceof RollershutterItem) { @@ -637,7 +690,7 @@ private String transform(String label, boolean matchTransform, @Nullable String } else if (w instanceof Slider) { if (i.getAcceptedDataTypes().contains(PercentType.class)) { returnState = itemState.as(PercentType.class); - } else { + } else if (!(itemState instanceof QuantityType)) { returnState = itemState.as(DecimalType.class); } } else if (w instanceof Switch sw) { @@ -1208,7 +1261,7 @@ public boolean getVisiblity(Widget w) { try { item = itemRegistry.getItem(itemName); } catch (ItemNotFoundException e) { - logger.error("Cannot retrieve visibility item {} for widget {}", rule.getItem(), + logger.warn("Cannot retrieve visibility item {} for widget {}", rule.getItem(), w.eClass().getInstanceTypeName()); // Default to visible! @@ -1325,12 +1378,13 @@ public void removeRegistryHook(RegistryHook hook) { // we require the item to define a dimension, otherwise no unit will be reported to the UIs. if (item instanceof NumberItem numberItem && numberItem.getDimension() != null) { - if (w.getLabel() == null) { + String pattern = getFormatPattern(w); + if (pattern == null || pattern.isBlank()) { // if no Label was assigned to the Widget we fallback to the items unit return numberItem.getUnitSymbol(); } - String unit = getUnitFromLabel(w.getLabel()); + String unit = getUnitFromPattern(pattern); if (!UnitUtils.UNIT_PLACEHOLDER.equals(unit)) { return unit; } @@ -1338,7 +1392,7 @@ public void removeRegistryHook(RegistryHook hook) { return numberItem.getUnitSymbol(); } } catch (ItemNotFoundException e) { - logger.debug("Failed to retrieve item during widget rendering: {}", e.getMessage()); + logger.warn("Failed to retrieve item during widget rendering, item does not exist: {}", e.getMessage()); } return ""; @@ -1354,14 +1408,13 @@ public void removeRegistryHook(RegistryHook hook) { return state; } - private @Nullable String getUnitFromLabel(@Nullable String label) { - if (label == null || label.isBlank()) { + private @Nullable String getUnitFromPattern(@Nullable String format) { + if (format == null || format.isBlank()) { return null; } - Matcher m = LABEL_PATTERN.matcher(label); - if (m.matches()) { - return m.group(1); - } - return null; + int index = format.lastIndexOf(" "); + String unit = index > 0 ? format.substring(index + 1) : null; + unit = UnitUtils.UNIT_PERCENT_FORMAT_STRING.equals(unit) ? "%" : unit; + return unit; } } diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java index 998a11d3c4c..a979543d4cf 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/items/ItemUIRegistry.java @@ -34,6 +34,7 @@ * @author Kai Kreuzer - Initial contribution * @author Chris Jackson - Initial contribution * @author Laurent Garnier - new method getIconColor + * @author Mark Herwege - new method getFormatPattern */ @NonNullByDefault public interface ItemUIRegistry extends ItemRegistry, ItemUIProvider { @@ -130,6 +131,15 @@ public interface ItemUIRegistry extends ItemRegistry, ItemUIProvider { @Nullable EObject getParent(Widget w); + /** + * Gets the format pattern for the widget value, retrieved from widget label, item label or item state description + * + * @param w Widget + * @return String with the format pattern + */ + @Nullable + String getFormatPattern(Widget w); + /** * Gets the label color for the widget. Checks conditional statements to * find the color based on the item value