[Sitemap] Add optional conditional rules for icon (#3820)

This allows dynamic icons based on items states even with non OH icon sources.
This also allows overwritting the default handling with state done by the icon servlet.

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

Related to openhab/openhab-webui#1938

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
lolodomo 2023-10-15 22:00:35 +02:00 committed by GitHub
parent 4001161810
commit 7281dca286
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 371 additions and 24 deletions

View File

@ -39,6 +39,7 @@ import org.openhab.core.model.sitemap.sitemap.Chart;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.Condition;
import org.openhab.core.model.sitemap.sitemap.Frame;
import org.openhab.core.model.sitemap.sitemap.IconRule;
import org.openhab.core.model.sitemap.sitemap.VisibilityRule;
import org.openhab.core.model.sitemap.sitemap.Widget;
import org.openhab.core.types.State;
@ -50,6 +51,7 @@ import org.openhab.core.ui.items.ItemUIRegistry;
* @author Kai Kreuzer - Initial contribution
* @author Laurent Garnier - Added support for icon color
* @author Laurent Garnier - Support added for multiple AND conditions in labelcolor/valuecolor/visibility
* @author Laurent Garnier - New widget icon parameter based on conditional rules
*/
public class PageChangeListener implements EventSubscriber {
@ -122,6 +124,10 @@ public class PageChangeListener implements EventSubscriber {
if (widget instanceof Frame frame) {
items.addAll(getAllItems(frame.getChildren()));
}
// now scan icon rules
for (IconRule rule : widget.getIconRules()) {
addItemsFromConditions(items, rule.getConditions());
}
// now scan visibility rules
for (VisibilityRule rule : widget.getVisibility()) {
addItemsFromConditions(items, rule.getConditions());
@ -194,7 +200,7 @@ public class PageChangeListener implements EventSubscriber {
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);
}
@ -208,6 +214,8 @@ public class PageChangeListener implements EventSubscriber {
event.pageId = pageId;
event.label = itemUIRegistry.getLabel(widget);
event.widgetId = itemUIRegistry.getWidgetId(widget);
event.icon = itemUIRegistry.getCategory(widget);
event.reloadIcon = widget.getStaticIcon() == null;
event.visibility = itemUIRegistry.getVisiblity(widget);
event.descriptionChanged = false;
// event.item contains the (potentially changed) data of the item belonging to
@ -248,11 +256,12 @@ public class PageChangeListener implements EventSubscriber {
return null;
}
private boolean definesVisibilityOrColor(Widget w, String name) {
private boolean definesVisibilityOrColorOrIcon(Widget w, String name) {
return w.getVisibility().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name))
|| w.getLabelColor().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name))
|| w.getValueColor().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name))
|| w.getIconColor().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name));
|| w.getIconColor().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name))
|| w.getIconRules().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name));
}
private boolean conditionsDependsOnItem(@Nullable EList<Condition> conditions, String name) {

View File

@ -82,6 +82,7 @@ import org.openhab.core.model.sitemap.sitemap.Chart;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.Condition;
import org.openhab.core.model.sitemap.sitemap.Frame;
import org.openhab.core.model.sitemap.sitemap.IconRule;
import org.openhab.core.model.sitemap.sitemap.Image;
import org.openhab.core.model.sitemap.sitemap.Input;
import org.openhab.core.model.sitemap.sitemap.LinkableWidget;
@ -137,6 +138,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
* @author Laurent Garnier - Added support for new sitemap element Buttongrid
* @author Laurent Garnier - Added icon field for mappings used for switch element
* @author Laurent Garnier - Support added for multiple AND conditions in labelcolor/valuecolor/visibility
* @author Laurent Garnier - New widget icon parameter based on conditional rules
*/
@Component(service = { RESTResource.class, EventSubscriber.class })
@JaxrsResource
@ -529,7 +531,7 @@ public class SitemapResource
}
bean.widgetId = widgetId;
bean.icon = itemUIRegistry.getCategory(widget);
bean.staticIcon = widget.getStaticIcon() != null;
bean.staticIcon = widget.getStaticIcon() != null || !widget.getIconRules().isEmpty();
bean.labelcolor = convertItemValueColor(itemUIRegistry.getLabelColor(widget), itemState);
bean.valuecolor = convertItemValueColor(itemUIRegistry.getValueColor(widget), itemState);
bean.iconcolor = convertItemValueColor(itemUIRegistry.getIconColor(widget), itemState);
@ -759,6 +761,8 @@ public class SitemapResource
if (widget instanceof Frame frame) {
items.addAll(getAllItems(frame.getChildren()));
}
// Consider items involved in any icon condition
items.addAll(getItemsInIconCond(widget.getIconRules()));
// Consider items involved in any visibility, labelcolor, valuecolor and iconcolor condition
items.addAll(getItemsInVisibilityCond(widget.getVisibility()));
items.addAll(getItemsInColorCond(widget.getLabelColor()));
@ -784,6 +788,14 @@ public class SitemapResource
return items;
}
private Set<GenericItem> getItemsInIconCond(EList<IconRule> ruleList) {
Set<GenericItem> items = new HashSet<>();
for (IconRule rule : ruleList) {
getItemsInConditions(rule.getConditions(), items);
}
return items;
}
private void getItemsInConditions(@Nullable EList<Condition> conditions, Set<GenericItem> items) {
if (conditions != null) {
for (Condition condition : conditions) {

View File

@ -19,6 +19,7 @@ import org.openhab.core.io.rest.core.item.EnrichedItemDTO;
*
* @author Kai Kreuzer - Initial contribution
* @author Laurent Garnier - New field iconcolor
* @author Laurent Garnier - New field reloadIcon
*/
public class SitemapWidgetEvent extends SitemapEvent {
@ -26,6 +27,7 @@ public class SitemapWidgetEvent extends SitemapEvent {
public String label;
public String icon;
public boolean reloadIcon;
public String labelcolor;
public String valuecolor;
public String iconcolor;

View File

@ -36,6 +36,11 @@ public class WidgetDTO {
public String label;
public String icon;
/**
* staticIcon is a boolean indicating if the widget state must be ignored when requesting the icon.
* It is set to true when the widget has either the staticIcon property set or the icon property set
* with conditional rules.
*/
public Boolean staticIcon;
public String labelcolor;
public String valuecolor;

View File

@ -50,6 +50,7 @@ import org.openhab.core.library.types.PercentType;
import org.openhab.core.model.sitemap.SitemapProvider;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.Condition;
import org.openhab.core.model.sitemap.sitemap.IconRule;
import org.openhab.core.model.sitemap.sitemap.Sitemap;
import org.openhab.core.model.sitemap.sitemap.VisibilityRule;
import org.openhab.core.model.sitemap.sitemap.Widget;
@ -62,6 +63,7 @@ import org.openhab.core.ui.items.ItemUIRegistry;
* Test aspects of the {@link SitemapResource}.
*
* @author Henning Treu - Initial contribution
* @author Laurent Garnier - Extended tests for static icon and icon based on conditional rules
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@ -80,10 +82,16 @@ public class SitemapResourceTest extends JavaTest {
private static final String LABEL_COLOR_ITEM_NAME = "labelColorItemName";
private static final String VALUE_COLOR_ITEM_NAME = "valueColorItemName";
private static final String ICON_COLOR_ITEM_NAME = "iconColorItemName";
private static final String ICON_ITEM_NAME = "iconItemName";
private static final String WIDGET1_LABEL = "widget 1";
private static final String WIDGET2_LABEL = "widget 2";
private static final String WIDGET3_LABEL = "widget 3";
private static final String WIDGET1_ID = "00";
private static final String WIDGET2_ID = "01";
private static final String WIDGET3_ID = "02";
private static final String WIDGET1_ICON = "icon1";
private static final String WIDGET2_ICON = "icon2";
private static final String WIDGET3_ICON = "icon3";
private static final String CLIENT_IP = "127.0.0.1";
private @NonNullByDefault({}) SitemapResource sitemapResource;
@ -93,6 +101,7 @@ public class SitemapResourceTest extends JavaTest {
private @NonNullByDefault({}) GenericItem labelColorItem;
private @NonNullByDefault({}) GenericItem valueColorItem;
private @NonNullByDefault({}) GenericItem iconColorItem;
private @NonNullByDefault({}) GenericItem iconItem;
private @Mock @NonNullByDefault({}) HttpHeaders headersMock;
private @Mock @NonNullByDefault({}) Sitemap defaultSitemapMock;
@ -121,6 +130,7 @@ public class SitemapResourceTest extends JavaTest {
labelColorItem = new TestItem(LABEL_COLOR_ITEM_NAME);
valueColorItem = new TestItem(VALUE_COLOR_ITEM_NAME);
iconColorItem = new TestItem(ICON_COLOR_ITEM_NAME);
iconItem = new TestItem(ICON_ITEM_NAME);
when(localeServiceMock.getLocale(null)).thenReturn(Locale.US);
@ -276,6 +286,30 @@ public class SitemapResourceTest extends JavaTest {
// return
}
@Test
public void whenLongPollingShouldObserveItemsFromIconConditions() {
ItemEvent itemEvent = mock(ItemEvent.class);
when(itemEvent.getItemName()).thenReturn(iconItem.getName());
new Thread(() -> {
try {
Thread.sleep(STATE_UPDATE_WAIT_TIME); // wait for the #getPageData call and listeners to attach to the
// item
sitemapResource.receive(itemEvent);
} catch (InterruptedException e) {
}
}).start();
// non-null is sufficient here.
when(headersMock.getRequestHeader(HTTP_HEADER_X_ATMOSPHERE_TRANSPORT)).thenReturn(List.of());
Response response = sitemapResource.getPageData(headersMock, null, SITEMAP_MODEL_NAME, SITEMAP_NAME, null,
false);
PageDTO pageDTO = (PageDTO) response.getEntity();
assertThat(pageDTO.timeout, is(false)); // assert that the item state change did trigger the blocking method to
// return
}
@Test
public void whenGetPageDataShouldReturnPageBean() throws ItemNotFoundException {
item.setState(new PercentType(50));
@ -294,13 +328,15 @@ public class SitemapResourceTest extends JavaTest {
assertThat(pageDTO.timeout, is(false));
assertThat(pageDTO.widgets, notNullValue());
assertThat((Collection<?>) pageDTO.widgets, hasSize(2));
assertThat((Collection<?>) pageDTO.widgets, hasSize(3));
assertThat(pageDTO.widgets.get(0).widgetId, is(WIDGET1_ID));
assertThat(pageDTO.widgets.get(0).label, is(WIDGET1_LABEL));
assertThat(pageDTO.widgets.get(0).labelcolor, is("GREEN"));
assertThat(pageDTO.widgets.get(0).valuecolor, is("BLUE"));
assertThat(pageDTO.widgets.get(0).iconcolor, is("ORANGE"));
assertThat(pageDTO.widgets.get(0).icon, is(WIDGET1_ICON));
assertThat(pageDTO.widgets.get(0).staticIcon, is(true));
assertThat(pageDTO.widgets.get(0).state, nullValue());
assertThat(pageDTO.widgets.get(0).item, notNullValue());
assertThat(pageDTO.widgets.get(0).item.name, is(ITEM_NAME));
@ -311,10 +347,24 @@ public class SitemapResourceTest extends JavaTest {
assertThat(pageDTO.widgets.get(1).labelcolor, nullValue());
assertThat(pageDTO.widgets.get(1).valuecolor, nullValue());
assertThat(pageDTO.widgets.get(1).iconcolor, nullValue());
assertThat(pageDTO.widgets.get(1).icon, is(WIDGET2_ICON));
assertThat(pageDTO.widgets.get(1).staticIcon, is(false));
assertThat(pageDTO.widgets.get(1).state, is("ON"));
assertThat(pageDTO.widgets.get(1).item, notNullValue());
assertThat(pageDTO.widgets.get(1).item.name, is(ITEM_NAME));
assertThat(pageDTO.widgets.get(1).item.state, is("50"));
assertThat(pageDTO.widgets.get(2).widgetId, is(WIDGET3_ID));
assertThat(pageDTO.widgets.get(2).label, is(WIDGET3_LABEL));
assertThat(pageDTO.widgets.get(2).labelcolor, nullValue());
assertThat(pageDTO.widgets.get(2).valuecolor, nullValue());
assertThat(pageDTO.widgets.get(2).iconcolor, nullValue());
assertThat(pageDTO.widgets.get(2).icon, is(WIDGET3_ICON));
assertThat(pageDTO.widgets.get(2).staticIcon, is(true));
assertThat(pageDTO.widgets.get(2).state, is("ON"));
assertThat(pageDTO.widgets.get(2).item, notNullValue());
assertThat(pageDTO.widgets.get(2).item.name, is(ITEM_NAME));
assertThat(pageDTO.widgets.get(2).item.state, is("50"));
}
private void configureItemUIRegistry(State state1, State state2) throws ItemNotFoundException {
@ -324,9 +374,10 @@ public class SitemapResourceTest extends JavaTest {
when(itemUIRegistryMock.getItem(LABEL_COLOR_ITEM_NAME)).thenReturn(labelColorItem);
when(itemUIRegistryMock.getItem(VALUE_COLOR_ITEM_NAME)).thenReturn(valueColorItem);
when(itemUIRegistryMock.getItem(ICON_COLOR_ITEM_NAME)).thenReturn(iconColorItem);
when(itemUIRegistryMock.getItem(ICON_ITEM_NAME)).thenReturn(iconItem);
when(itemUIRegistryMock.getWidgetId(widgets.get(0))).thenReturn(WIDGET1_ID);
when(itemUIRegistryMock.getCategory(widgets.get(0))).thenReturn("");
when(itemUIRegistryMock.getCategory(widgets.get(0))).thenReturn(WIDGET1_ICON);
when(itemUIRegistryMock.getLabel(widgets.get(0))).thenReturn(WIDGET1_LABEL);
when(itemUIRegistryMock.getVisiblity(widgets.get(0))).thenReturn(true);
when(itemUIRegistryMock.getLabelColor(widgets.get(0))).thenReturn("GREEN");
@ -335,13 +386,22 @@ public class SitemapResourceTest extends JavaTest {
when(itemUIRegistryMock.getState(widgets.get(0))).thenReturn(state1);
when(itemUIRegistryMock.getWidgetId(widgets.get(1))).thenReturn(WIDGET2_ID);
when(itemUIRegistryMock.getCategory(widgets.get(1))).thenReturn("");
when(itemUIRegistryMock.getCategory(widgets.get(1))).thenReturn(WIDGET2_ICON);
when(itemUIRegistryMock.getLabel(widgets.get(1))).thenReturn(WIDGET2_LABEL);
when(itemUIRegistryMock.getVisiblity(widgets.get(1))).thenReturn(true);
when(itemUIRegistryMock.getLabelColor(widgets.get(1))).thenReturn(null);
when(itemUIRegistryMock.getValueColor(widgets.get(1))).thenReturn(null);
when(itemUIRegistryMock.getIconColor(widgets.get(1))).thenReturn(null);
when(itemUIRegistryMock.getState(widgets.get(1))).thenReturn(state2);
when(itemUIRegistryMock.getWidgetId(widgets.get(2))).thenReturn(WIDGET3_ID);
when(itemUIRegistryMock.getCategory(widgets.get(2))).thenReturn(WIDGET3_ICON);
when(itemUIRegistryMock.getLabel(widgets.get(2))).thenReturn(WIDGET3_LABEL);
when(itemUIRegistryMock.getVisiblity(widgets.get(2))).thenReturn(true);
when(itemUIRegistryMock.getLabelColor(widgets.get(2))).thenReturn(null);
when(itemUIRegistryMock.getValueColor(widgets.get(2))).thenReturn(null);
when(itemUIRegistryMock.getIconColor(widgets.get(2))).thenReturn(null);
when(itemUIRegistryMock.getState(widgets.get(2))).thenReturn(state2);
}
private EList<Widget> initSitemapWidgets() {
@ -355,6 +415,19 @@ public class SitemapResourceTest extends JavaTest {
when(w1.eClass()).thenReturn(sliderEClass);
when(w1.getLabel()).thenReturn(WIDGET1_LABEL);
when(w1.getItem()).thenReturn(ITEM_NAME);
when(w1.getIcon()).thenReturn(null);
when(w1.getStaticIcon()).thenReturn(null);
// add icon rules to the mock widget:
IconRule iconRule = mock(IconRule.class);
Condition conditon0 = mock(Condition.class);
when(conditon0.getItem()).thenReturn(ICON_ITEM_NAME);
EList<Condition> conditions0 = new BasicEList<>();
conditions0.add(conditon0);
when(iconRule.getConditions()).thenReturn(conditions0);
EList<IconRule> iconRules = new BasicEList<>();
iconRules.add(iconRule);
when(w1.getIconRules()).thenReturn(iconRules);
// add visibility rules to the mock widget:
VisibilityRule visibilityRule = mock(VisibilityRule.class);
@ -400,6 +473,7 @@ public class SitemapResourceTest extends JavaTest {
iconColors.add(iconColor);
when(w1.getIconColor()).thenReturn(iconColors);
iconRules = new BasicEList<>();
visibilityRules = new BasicEList<>();
labelColors = new BasicEList<>();
valueColors = new BasicEList<>();
@ -412,14 +486,30 @@ public class SitemapResourceTest extends JavaTest {
when(w2.eClass()).thenReturn(switchEClass);
when(w2.getLabel()).thenReturn(WIDGET2_LABEL);
when(w2.getItem()).thenReturn(ITEM_NAME);
when(w2.getIcon()).thenReturn(WIDGET2_ICON);
when(w2.getStaticIcon()).thenReturn(null);
when(w2.getIconRules()).thenReturn(iconRules);
when(w2.getVisibility()).thenReturn(visibilityRules);
when(w2.getLabelColor()).thenReturn(labelColors);
when(w2.getValueColor()).thenReturn(valueColors);
when(w2.getIconColor()).thenReturn(iconColors);
EList<Widget> widgets = new BasicEList<>(2);
Widget w3 = mock(Widget.class);
when(w3.eClass()).thenReturn(switchEClass);
when(w3.getLabel()).thenReturn(WIDGET3_LABEL);
when(w3.getItem()).thenReturn(ITEM_NAME);
when(w3.getIcon()).thenReturn(null);
when(w3.getStaticIcon()).thenReturn(WIDGET3_ICON);
when(w3.getIconRules()).thenReturn(iconRules);
when(w3.getVisibility()).thenReturn(visibilityRules);
when(w3.getLabelColor()).thenReturn(labelColors);
when(w3.getValueColor()).thenReturn(valueColors);
when(w3.getIconColor()).thenReturn(iconColors);
EList<Widget> widgets = new BasicEList<>(3);
widgets.add(w1);
widgets.add(w2);
widgets.add(w3);
return widgets;
}

View File

@ -25,7 +25,9 @@ LinkableWidget:
Frame:
{Frame} 'Frame' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? &
@ -33,7 +35,9 @@ Frame:
Text:
{Text} 'Text' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? &
@ -41,7 +45,9 @@ Text:
Group:
'Group' (('item=' item=GroupItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? &
@ -49,7 +55,9 @@ Group:
Image:
'Image' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('url=' url=STRING)? & ('refresh=' refresh=INT)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
@ -58,7 +66,9 @@ Image:
Video:
'Video' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('url=' url=STRING) & ('encoding=' encoding=STRING)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
@ -67,7 +77,9 @@ Video:
Chart:
'Chart' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('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)*) ']')? &
@ -78,7 +90,9 @@ Chart:
Webview:
'Webview' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('height=' height=INT)? & ('url=' url=STRING) &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
@ -87,7 +101,9 @@ Webview:
Switch:
'Switch' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('mappings=[' mappings+=Mapping (',' mappings+=Mapping)* ']')? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
@ -96,7 +112,9 @@ Switch:
Mapview:
'Mapview' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('height=' height=INT)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
@ -105,7 +123,9 @@ Mapview:
Slider:
'Slider' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('sendFrequency=' frequency=INT)? & (switchEnabled?='switchSupport')? &
('minValue=' minValue=Number)? & ('maxValue=' maxValue=Number)? & ('step=' step=Number)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
@ -115,7 +135,9 @@ Slider:
Selection:
'Selection' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('mappings=[' mappings+=Mapping (',' mappings+=Mapping)* ']')? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
@ -124,7 +146,9 @@ Selection:
Setpoint:
'Setpoint' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('minValue=' minValue=Number)? & ('maxValue=' maxValue=Number)? & ('step=' step=Number)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
@ -133,7 +157,9 @@ Setpoint:
Colorpicker:
'Colorpicker' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('sendFrequency=' frequency=INT)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
@ -142,7 +168,9 @@ Colorpicker:
Input:
'Input' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('inputHint=' inputHint=STRING)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
@ -151,7 +179,9 @@ Input:
Buttongrid:
'Buttongrid' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('columns=' columns=INT) &
('buttons=[' buttons+=Button (',' buttons+=Button)* ']') &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
@ -161,7 +191,9 @@ Buttongrid:
Default:
'Default' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &
(('icon=' icon=Icon) | ('staticIcon=' staticIcon=Icon))? &
(('icon=' icon=Icon) |
('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') |
('staticIcon=' staticIcon=Icon))? &
('height=' height=INT)? &
('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? &
('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? &
@ -193,6 +225,9 @@ IconName:
ColorArray:
((conditions+=Condition ('AND' conditions+=Condition)*) '=')? (arg=STRING);
IconRule:
((conditions+=Condition ('AND' conditions+=Condition)*) '=')? (arg=Icon);
Condition:
(item=ID)? (condition=('==' | '>' | '<' | '>=' | '<=' | '!='))? (sign=('-' | '+'))? (state=XState);

View File

@ -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.Button;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.IconRule;
import org.openhab.core.model.sitemap.sitemap.LinkableWidget;
import org.openhab.core.model.sitemap.sitemap.Mapping;
import org.openhab.core.model.sitemap.sitemap.Sitemap;
@ -47,6 +48,7 @@ import org.openhab.core.model.sitemap.sitemap.impl.ConditionImpl;
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.IconRuleImpl;
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;
@ -95,6 +97,7 @@ public class UIComponentSitemapProvider implements SitemapProvider, RegistryChan
.compile("(?<item>[A-Za-z]\\w*)\\s*(?<condition>==|!=|<=|>=|<|>)\\s*(?<sign>\\+|-)?(?<state>.+)");
private static final Pattern COLOR_PATTERN = Pattern.compile(
"((?<item>[A-Za-z]\\w*)?\\s*((?<condition>==|!=|<=|>=|<|>)\\s*(?<sign>\\+|-)?(?<state>[^=]*[^= ]+))?\\s*=)?\\s*(?<arg>\\S+)");
private static final Pattern ICON_PATTERN = COLOR_PATTERN;
private Map<String, Sitemap> sitemaps = new HashMap<>();
private @Nullable UIComponentRegistryFactory componentRegistryFactory;
@ -296,6 +299,7 @@ public class UIComponentSitemapProvider implements SitemapProvider, RegistryChan
addLabelColor(widget.getLabelColor(), component);
addValueColor(widget.getValueColor(), component);
addIconColor(widget.getIconColor(), component);
addIconRules(widget.getIconRules(), component);
}
return widget;
@ -429,6 +433,29 @@ public class UIComponentSitemapProvider implements SitemapProvider, RegistryChan
}
}
private void addIconRules(EList<IconRule> 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()) {
IconRuleImpl iconRule = (IconRuleImpl) SitemapFactory.eINSTANCE.createIconRule();
ConditionImpl condition = (ConditionImpl) SitemapFactory.eINSTANCE.createCondition();
condition.setItem(matcher.group("item"));
condition.setCondition(matcher.group("condition"));
condition.setSign(matcher.group("sign"));
condition.setState(matcher.group("state"));
iconRule.eSet(SitemapPackage.ICON_RULE__CONDITIONS, condition);
iconRule.setArg(matcher.group("arg"));
iconDef.add(iconRule);
} else {
logger.warn("Syntax error in icon rule '{}' for widget {}", sourceIcon, component.getType());
}
}
}
}
}
@Override
public void addModelChangeListener(ModelRepositoryChangeListener listener) {
modelChangeListeners.add(listener);

View File

@ -68,6 +68,7 @@ import org.openhab.core.library.types.StringType;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.Default;
import org.openhab.core.model.sitemap.sitemap.Group;
import org.openhab.core.model.sitemap.sitemap.IconRule;
import org.openhab.core.model.sitemap.sitemap.LinkableWidget;
import org.openhab.core.model.sitemap.sitemap.Mapping;
import org.openhab.core.model.sitemap.sitemap.Sitemap;
@ -110,6 +111,7 @@ import org.slf4j.LoggerFactory;
* @author Laurent Garnier - new method getIconColor
* @author Mark Herwege - new method getFormatPattern(widget), clean pattern
* @author Laurent Garnier - Support added for multiple AND conditions in labelcolor/valuecolor/visibility
* @author Laurent Garnier - new icon parameter based on conditional rules
*/
@NonNullByDefault
@Component(immediate = true, configurationPid = "org.openhab.sitemap", //
@ -639,11 +641,14 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
// the default is the widget type name, e.g. "switch"
String category = widgetTypeName.toLowerCase();
String conditionalIcon = getConditionalIcon(w);
// if an icon is defined for the widget, use it
if (w.getIcon() != null) {
category = w.getIcon();
} else if (w.getStaticIcon() != null) {
category = w.getStaticIcon();
} else if (conditionalIcon != null) {
category = conditionalIcon;
} else {
// otherwise check if any item ui provider provides an icon for this item
String itemName = w.getItem();
@ -801,6 +806,7 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
target.getLabelColor().addAll(EcoreUtil.copyAll(source.getLabelColor()));
target.getValueColor().addAll(EcoreUtil.copyAll(source.getValueColor()));
target.getIconColor().addAll(EcoreUtil.copyAll(source.getIconColor()));
target.getIconRules().addAll(EcoreUtil.copyAll(source.getIconRules()));
}
/**
@ -1220,6 +1226,42 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
return false;
}
@Override
public @Nullable String getConditionalIcon(Widget w) {
List<IconRule> ruleList = w.getIconRules();
// Sanity check
if (ruleList == null || ruleList.isEmpty()) {
return null;
}
logger.debug("Checking icon for widget '{}'.", w.getLabel());
String icon = null;
// Loop through all elements looking for the definition associated
// with the supplied value
for (IconRule rule : ruleList) {
if (allConditionsOk(rule.getConditions(), w)) {
// We have the icon for this value - break!
icon = rule.getArg();
break;
}
}
if (icon == null) {
logger.debug("No icon found for widget '{}'.", w.getLabel());
return null;
}
// Remove quotes off the icon - if they exist
if (icon.startsWith("\"") && icon.endsWith("\"")) {
icon = icon.substring(1, icon.length() - 1);
}
logger.debug("icon for widget '{}' is '{}'.", w.getLabel(), icon);
return icon;
}
private boolean allConditionsOk(@Nullable List<org.openhab.core.model.sitemap.sitemap.Condition> conditions,
Widget w) {
boolean allConditionsOk = true;

View File

@ -35,6 +35,7 @@ import org.openhab.core.types.State;
* @author Chris Jackson - Initial contribution
* @author Laurent Garnier - new method getIconColor
* @author Mark Herwege - new method getFormatPattern
* @author Laurent Garnier - new method getConditionalIcon
*/
@NonNullByDefault
public interface ItemUIRegistry extends ItemRegistry, ItemUIProvider {
@ -170,6 +171,17 @@ public interface ItemUIRegistry extends ItemRegistry, ItemUIProvider {
@Nullable
String getIconColor(Widget w);
/**
* Gets the icon for the widget. Checks conditional statements to
* find the icon based on the item value
*
* @param w Widget
* @return the icon reference or null in case no conditional statement is defined or no conditional statement is
* fulfilled.
*/
@Nullable
String getConditionalIcon(Widget w);
/**
* Gets the widget visibility based on the item state
*

View File

@ -27,6 +27,7 @@ import java.util.TimeZone;
import javax.measure.quantity.Temperature;
import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -62,6 +63,7 @@ import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.Colorpicker;
import org.openhab.core.model.sitemap.sitemap.Condition;
import org.openhab.core.model.sitemap.sitemap.Group;
import org.openhab.core.model.sitemap.sitemap.IconRule;
import org.openhab.core.model.sitemap.sitemap.Image;
import org.openhab.core.model.sitemap.sitemap.Mapping;
import org.openhab.core.model.sitemap.sitemap.Mapview;
@ -86,6 +88,7 @@ import org.openhab.core.ui.items.ItemUIProvider;
/**
* @author Kai Kreuzer - Initial contribution
* @author Laurent Garnier - Tests updated to consider multiple AND conditions + tests added for getVisiblity
* @author Laurent Garnier - Tests added for getCategory
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@ -1184,4 +1187,114 @@ public class ItemUIRegistryImplTest {
assertFalse(uiRegistry.getVisiblity(widgetMock));
}
@Test
public void getCategoryWhenIconSetWithoutRules() {
EClass textEClass = mock(EClass.class);
when(textEClass.getName()).thenReturn("text");
when(textEClass.getInstanceTypeName()).thenReturn("org.openhab.core.model.sitemap.Text");
when(widgetMock.eClass()).thenReturn(textEClass);
when(widgetMock.getIcon()).thenReturn("temperature");
when(widgetMock.getStaticIcon()).thenReturn(null);
when(widgetMock.getIconRules()).thenReturn(null);
String icon = uiRegistry.getCategory(widgetMock);
assertEquals("temperature", icon);
}
@Test
public void getCategoryWhenIconSetWithRules() {
EClass textEClass = mock(EClass.class);
when(textEClass.getName()).thenReturn("text");
when(textEClass.getInstanceTypeName()).thenReturn("org.openhab.core.model.sitemap.Text");
when(widgetMock.eClass()).thenReturn(textEClass);
when(widgetMock.getIcon()).thenReturn(null);
when(widgetMock.getStaticIcon()).thenReturn(null);
Condition conditon = mock(Condition.class);
when(conditon.getState()).thenReturn("21");
when(conditon.getCondition()).thenReturn(">=");
Condition conditon2 = mock(Condition.class);
when(conditon2.getState()).thenReturn("24");
when(conditon2.getCondition()).thenReturn("<");
BasicEList<Condition> conditions = new BasicEList<>();
conditions.add(conditon);
conditions.add(conditon2);
IconRule rule = mock(IconRule.class);
when(rule.getConditions()).thenReturn(conditions);
when(rule.getArg()).thenReturn("temperature");
BasicEList<IconRule> rules = new BasicEList<>();
rules.add(rule);
BasicEList<Condition> conditions2 = new BasicEList<>();
IconRule rule2 = mock(IconRule.class);
when(rule2.getConditions()).thenReturn(conditions2);
when(rule2.getArg()).thenReturn("humidity");
rules.add(rule2);
when(widgetMock.getIconRules()).thenReturn(rules);
when(itemMock.getState()).thenReturn(new DecimalType(20.9));
String icon = uiRegistry.getCategory(widgetMock);
assertEquals("humidity", icon);
when(itemMock.getState()).thenReturn(new DecimalType(21.0));
icon = uiRegistry.getCategory(widgetMock);
assertEquals("temperature", icon);
when(itemMock.getState()).thenReturn(new DecimalType(23.5));
icon = uiRegistry.getCategory(widgetMock);
assertEquals("temperature", icon);
when(itemMock.getState()).thenReturn(new DecimalType(24.0));
icon = uiRegistry.getCategory(widgetMock);
assertEquals("humidity", icon);
}
@Test
public void getCategoryWhenStaticIconSet() {
EClass textEClass = mock(EClass.class);
when(textEClass.getName()).thenReturn("text");
when(textEClass.getInstanceTypeName()).thenReturn("org.openhab.core.model.sitemap.Text");
when(widgetMock.eClass()).thenReturn(textEClass);
when(widgetMock.getIcon()).thenReturn(null);
when(widgetMock.getStaticIcon()).thenReturn("temperature");
when(widgetMock.getIconRules()).thenReturn(null);
String icon = uiRegistry.getCategory(widgetMock);
assertEquals("temperature", icon);
}
@Test
public void getCategoryWhenIconSetOnItem() {
EClass textEClass = mock(EClass.class);
when(textEClass.getName()).thenReturn("text");
when(textEClass.getInstanceTypeName()).thenReturn("org.openhab.core.model.sitemap.Text");
when(widgetMock.eClass()).thenReturn(textEClass);
when(widgetMock.getIcon()).thenReturn(null);
when(widgetMock.getStaticIcon()).thenReturn(null);
when(widgetMock.getIconRules()).thenReturn(null);
when(itemMock.getCategory()).thenReturn("temperature");
String icon = uiRegistry.getCategory(widgetMock);
assertEquals("temperature", icon);
}
@Test
public void getCategoryDefaultIcon() {
EClass textEClass = mock(EClass.class);
when(textEClass.getName()).thenReturn("text");
when(textEClass.getInstanceTypeName()).thenReturn("org.openhab.core.model.sitemap.Text");
when(widgetMock.eClass()).thenReturn(textEClass);
when(widgetMock.getIcon()).thenReturn(null);
when(widgetMock.getStaticIcon()).thenReturn(null);
when(widgetMock.getIconRules()).thenReturn(null);
when(itemMock.getCategory()).thenReturn(null);
String icon = uiRegistry.getCategory(widgetMock);
assertEquals("text", icon);
}
}