Skip to content

Commit

Permalink
[sitemap] Extend icon parameter with optional conditional rules
Browse files Browse the repository at this point in the history
Related to openhab/openhab-webui#1938

Allow dynamic icon based on other item states.
Allow dynamic icon even with non OH icon sources.

Example: icon=[item1>0=temperature,==0=material::settings,f7::house]

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
  • Loading branch information
lolodomo committed Aug 14, 2023
1 parent c12067a commit c343ca0
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.openhab.core.model.sitemap.sitemap.Chart;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.Frame;
import org.openhab.core.model.sitemap.sitemap.IconArray;
import org.openhab.core.model.sitemap.sitemap.VisibilityRule;
import org.openhab.core.model.sitemap.sitemap.Widget;
import org.openhab.core.types.State;
Expand Down Expand Up @@ -119,6 +120,10 @@ private Set<Item> getAllItems(EList<Widget> widgets) {
if (widget instanceof Frame frame) {
items.addAll(getAllItems(frame.getChildren()));
}
// now scan dynamic icon rules
for (IconArray rule : widget.getDynamicIcon()) {
addItemWithName(items, rule.getItem());
}
// now scan visibility rules
for (VisibilityRule rule : widget.getVisibility()) {
addItemWithName(items, rule.getItem());
Expand All @@ -131,7 +136,7 @@ private Set<Item> getAllItems(EList<Widget> widgets) {
for (ColorArray rule : widget.getValueColor()) {
addItemWithName(items, rule.getItem());
}
// now scan value icon rules
// now scan icon color rules
for (ColorArray rule : widget.getIconColor()) {
addItemWithName(items, rule.getItem());
}
Expand Down Expand Up @@ -183,7 +188,7 @@ private Set<SitemapEvent> constructSitemapEvents(Item item, State state, List<Wi
if (!skipWidget && w instanceof Chart chartWidget) {
skipWidget = chartWidget.getRefresh() > 0;
}
if (!skipWidget || definesVisibilityOrColor(w, item.getName())) {
if (!skipWidget || definesVisibilityOrColorOrIcon(w, item.getName())) {
SitemapWidgetEvent event = constructSitemapEventForWidget(item, state, w);
events.add(event);
}
Expand All @@ -197,6 +202,9 @@ private SitemapWidgetEvent constructSitemapEventForWidget(Item item, State state
event.pageId = pageId;
event.label = itemUIRegistry.getLabel(widget);
event.widgetId = itemUIRegistry.getWidgetId(widget);
if (widget.getStaticIcon() == null) {
event.icon = itemUIRegistry.getCategory(widget);
}
event.visibility = itemUIRegistry.getVisiblity(widget);
event.descriptionChanged = false;
// event.item contains the (potentially changed) data of the item belonging to
Expand Down Expand Up @@ -237,11 +245,12 @@ private Item getItemForWidget(Widget w) {
return null;
}

private boolean definesVisibilityOrColor(Widget w, String name) {
private boolean definesVisibilityOrColorOrIcon(Widget w, String name) {
return w.getVisibility().stream().anyMatch(r -> name.equals(r.getItem()))
|| w.getLabelColor().stream().anyMatch(r -> name.equals(r.getItem()))
|| w.getValueColor().stream().anyMatch(r -> name.equals(r.getItem()))
|| w.getIconColor().stream().anyMatch(r -> name.equals(r.getItem()));
|| w.getIconColor().stream().anyMatch(r -> name.equals(r.getItem()))
|| w.getDynamicIcon().stream().anyMatch(r -> name.equals(r.getItem()));
}

public void sitemapContentChanged(EList<Widget> widgets) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
import org.openhab.core.model.sitemap.sitemap.Chart;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.Frame;
import org.openhab.core.model.sitemap.sitemap.IconArray;
import org.openhab.core.model.sitemap.sitemap.Image;
import org.openhab.core.model.sitemap.sitemap.Input;
import org.openhab.core.model.sitemap.sitemap.LinkableWidget;
Expand Down Expand Up @@ -523,7 +524,7 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null
}
bean.widgetId = widgetId;
bean.icon = itemUIRegistry.getCategory(widget);
bean.staticIcon = widget.getStaticIcon() != null;
bean.staticIcon = widget.getStaticIcon() != null || widget.getDynamicIcon() != null;
bean.labelcolor = convertItemValueColor(itemUIRegistry.getLabelColor(widget), itemState);
bean.valuecolor = convertItemValueColor(itemUIRegistry.getValueColor(widget), itemState);
bean.iconcolor = convertItemValueColor(itemUIRegistry.getIconColor(widget), itemState);
Expand Down Expand Up @@ -741,6 +742,8 @@ private Set<GenericItem> getAllItems(EList<Widget> widgets) {
if (widget instanceof Frame frame) {
items.addAll(getAllItems(frame.getChildren()));
}
// Consider items involved in any icon condition
items.addAll(getItemsInIconCond(widget.getDynamicIcon()));
// Consider items involved in any visibility, labelcolor, valuecolor and iconcolor condition
items.addAll(getItemsInVisibilityCond(widget.getVisibility()));
items.addAll(getItemsInColorCond(widget.getLabelColor()));
Expand Down Expand Up @@ -786,6 +789,24 @@ private Set<GenericItem> getItemsInColorCond(EList<ColorArray> colorList) {
return items;
}

private Set<GenericItem> getItemsInIconCond(EList<IconArray> ruleList) {
Set<GenericItem> items = new HashSet<>();
for (IconArray rule : ruleList) {
String itemName = rule.getItem();
if (itemName != null) {
try {
Item item = itemUIRegistry.getItem(itemName);
if (item instanceof GenericItem genericItem) {
items.add(genericItem);
}
} catch (ItemNotFoundException e) {
// ignore
}
}
}
return items;
}

@Override
public Set<String> getSubscribedEventTypes() {
return Set.of(ItemStateChangedEvent.TYPE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ private EList<Widget> initSitemapWidgets() {
when(w1.eClass()).thenReturn(sliderEClass);
when(w1.getLabel()).thenReturn(WIDGET1_LABEL);
when(w1.getItem()).thenReturn(ITEM_NAME);
when(w1.getDynamicIcon()).thenReturn(new BasicEList<>());
when(w1.getStaticIcon()).thenReturn(null);

// add visibility rules to the mock widget:
VisibilityRule visibilityRule = mock(VisibilityRule.class);
Expand Down Expand Up @@ -371,6 +373,8 @@ private EList<Widget> initSitemapWidgets() {
when(w2.eClass()).thenReturn(switchEClass);
when(w2.getLabel()).thenReturn(WIDGET2_LABEL);
when(w2.getItem()).thenReturn(ITEM_NAME);
when(w2.getDynamicIcon()).thenReturn(new BasicEList<>());
when(w2.getStaticIcon()).thenReturn(null);
when(w2.getVisibility()).thenReturn(visibilityRules);
when(w2.getLabelColor()).thenReturn(labelColors);
when(w2.getValueColor()).thenReturn(valueColors);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,39 @@ LinkableWidget:

Frame:
{Frame} 'Frame' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)* ']'))? &
('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)* ']'))?);

Text:
{Text} 'Text' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)* ']'))? &
('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)* ']'))?);

Group:
'Group' (('item=' item=GroupItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)* ']'))? &
('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)* ']'))?);

Image:
'Image' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('url=' url=STRING)? & ('refresh=' refresh=INT)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
Expand All @@ -58,7 +66,9 @@ Image:

Video:
'Video' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('url=' url=STRING) & ('encoding=' encoding=STRING)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
Expand All @@ -67,7 +77,9 @@ Video:

Chart:
'Chart' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('service=' service=STRING)? & ('refresh=' refresh=INT)? & ('period=' period=ID) &
('legend=' legend=BOOLEAN_OBJECT)? & ('forceasitem=' forceAsItem=BOOLEAN_OBJECT)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
Expand All @@ -78,7 +90,9 @@ Chart:

Webview:
'Webview' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('height=' height=INT)? & ('url=' url=STRING) &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
Expand All @@ -87,7 +101,9 @@ Webview:

Switch:
'Switch' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('mappings=[' mappings+=Mapping (',' mappings+=Mapping)* ']')? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
Expand All @@ -96,7 +112,9 @@ Switch:

Mapview:
'Mapview' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('height=' height=INT)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
Expand All @@ -105,7 +123,9 @@ Mapview:

Slider:
'Slider' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('sendFrequency=' frequency=INT)? & (switchEnabled?='switchSupport')? &
('minValue=' minValue=Number)? & ('maxValue=' maxValue=Number)? & ('step=' step=Number)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
Expand All @@ -115,7 +135,9 @@ Slider:

Selection:
'Selection' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('mappings=[' mappings+=Mapping (',' mappings+=Mapping)* ']')? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
Expand All @@ -124,7 +146,9 @@ Selection:

Setpoint:
'Setpoint' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('minValue=' minValue=Number)? & ('maxValue=' maxValue=Number)? & ('step=' step=Number)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
Expand All @@ -133,7 +157,9 @@ Setpoint:

Colorpicker:
'Colorpicker' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('sendFrequency=' frequency=INT)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
Expand All @@ -142,7 +168,9 @@ Colorpicker:

Input:
'Input' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('inputHint=' inputHint=STRING)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
Expand All @@ -151,7 +179,9 @@ Input:

Default:
'Default' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (dynamicIcon+=IconArray (',' dynamicIcon+=IconArray)* ']')) |
('staticIcon=' staticIcon=Icon))? &
('height=' height=INT)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)* ']'))? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)* ']'))? &
Expand Down Expand Up @@ -181,6 +211,10 @@ ColorArray:
((item=ID)? (condition=('==' | '>' | '<' | '>=' | '<=' | '!='))? (sign=('-' | '+'))? (state=XState) '=')?
(arg=STRING);

IconArray:
((item=ID)? (condition=('==' | '>' | '<' | '>=' | '<=' | '!='))? (sign=('-' | '+'))? (state=XState) '=')?
(arg=Icon);

Command returns ecore::EString:
Number | ID | STRING;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.openhab.core.model.core.ModelRepositoryChangeListener;
import org.openhab.core.model.sitemap.SitemapProvider;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.IconArray;
import org.openhab.core.model.sitemap.sitemap.LinkableWidget;
import org.openhab.core.model.sitemap.sitemap.Mapping;
import org.openhab.core.model.sitemap.sitemap.Sitemap;
Expand All @@ -44,6 +45,7 @@
import org.openhab.core.model.sitemap.sitemap.impl.DefaultImpl;
import org.openhab.core.model.sitemap.sitemap.impl.FrameImpl;
import org.openhab.core.model.sitemap.sitemap.impl.GroupImpl;
import org.openhab.core.model.sitemap.sitemap.impl.IconArrayImpl;
import org.openhab.core.model.sitemap.sitemap.impl.ImageImpl;
import org.openhab.core.model.sitemap.sitemap.impl.InputImpl;
import org.openhab.core.model.sitemap.sitemap.impl.MappingImpl;
Expand Down Expand Up @@ -90,6 +92,8 @@ public class UIComponentSitemapProvider implements SitemapProvider, RegistryChan
.compile("(?<item>[A-Za-z]\\w*)\\s*(?<condition>==|!=|<=|>=|<|>)\\s*(?<sign>\\+|-)?(?<state>\\S+)");
private static final Pattern COLOR_PATTERN = Pattern.compile(
"((?<item>[A-Za-z]\\w*)?\\s*((?<condition>==|!=|<=|>=|<|>)\\s*(?<sign>\\+|-)?(?<state>\\S+))?\\s*=)?\\s*(?<arg>\\S+)");
private static final Pattern ICON_PATTERN = Pattern.compile(
"((?<item>[A-Za-z]\\w*)?\\s*((?<condition>==|!=|<=|>=|<|>)\\s*(?<sign>\\+|-)?(?<state>\\S+))?\\s*=)?\\s*(?<arg>\\S+)");

private Map<String, Sitemap> sitemaps = new HashMap<>();
private @Nullable UIComponentRegistryFactory componentRegistryFactory;
Expand Down Expand Up @@ -285,6 +289,7 @@ protected Sitemap buildSitemap(RootUIComponent rootComponent) {
addLabelColor(widget.getLabelColor(), component);
addValueColor(widget.getValueColor(), component);
addIconColor(widget.getIconColor(), component);
addDynamicIcon(widget.getDynamicIcon(), component);
}

return widget;
Expand Down Expand Up @@ -389,6 +394,27 @@ private void addColor(EList<ColorArray> color, UIComponent component, String key
}
}

private void addDynamicIcon(EList<IconArray> iconDef, UIComponent component) {
if (component.getConfig() != null && component.getConfig().containsKey("icon")) {
for (Object sourceIcon : (Collection<?>) component.getConfig().get("icon")) {
if (sourceIcon instanceof String) {
Matcher matcher = ICON_PATTERN.matcher(sourceIcon.toString());
if (matcher.matches()) {
IconArrayImpl iconArray = (IconArrayImpl) SitemapFactory.eINSTANCE.createIconArray();
iconArray.setItem(matcher.group("item"));
iconArray.setCondition(matcher.group("condition"));
iconArray.setSign(matcher.group("sign"));
iconArray.setState(matcher.group("state"));
iconArray.setArg(matcher.group("arg"));
iconDef.add(iconArray);
} else {
logger.warn("Syntax error in icon rule '{}' for widget {}", sourceIcon, component.getType());
}
}
}
}
}

@Override
public void addModelChangeListener(ModelRepositoryChangeListener listener) {
modelChangeListeners.add(listener);
Expand Down
Loading

0 comments on commit c343ca0

Please sign in to comment.