From 6b3e5e95a058cc6381be7357870e87e68d56078b Mon Sep 17 00:00:00 2001 From: Laurent Garnier Date: Mon, 2 Oct 2023 20:03:05 +0200 Subject: [PATCH] [Sitemap] Add optional conditional rules for icon 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 --- .../sitemap/internal/PageChangeListener.java | 30 ++++- .../sitemap/internal/SitemapResource.java | 33 ++++- .../sitemap/internal/SitemapWidgetEvent.java | 8 ++ .../io/rest/sitemap/internal/WidgetDTO.java | 5 + .../sitemap/internal/SitemapResourceTest.java | 99 ++++++++++++++- .../openhab/core/model/sitemap/Sitemap.xtext | 70 ++++++++--- .../UIComponentSitemapProvider.java | 28 +++++ .../ui/internal/items/ItemUIRegistryImpl.java | 86 +++++++++++++ .../openhab/core/ui/items/ItemUIRegistry.java | 12 ++ .../items/ItemUIRegistryImplTest.java | 114 ++++++++++++++++++ 10 files changed, 461 insertions(+), 24 deletions(-) diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java index 30aa6bc5e92..5d06af19137 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/PageChangeListener.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import org.eclipse.emf.common.util.EList; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.events.Event; import org.openhab.core.events.EventSubscriber; @@ -36,7 +37,9 @@ import org.openhab.core.library.CoreItemFactory; 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; @@ -47,6 +50,7 @@ * * @author Kai Kreuzer - Initial contribution * @author Laurent Garnier - Added support for icon color + * @author Laurent Garnier - New widget icon parameter based on conditional rules */ public class PageChangeListener implements EventSubscriber { @@ -119,6 +123,10 @@ private Set getAllItems(EList widgets) { 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()) { addItemWithName(items, rule.getItem()); @@ -140,6 +148,14 @@ private Set getAllItems(EList widgets) { return items; } + private void addItemsFromConditions(Set items, @Nullable EList conditions) { + if (conditions != null) { + for (Condition condition : conditions) { + addItemWithName(items, condition.getItem()); + } + } + } + private void addItemWithName(Set items, String itemName) { if (itemName != null) { try { @@ -183,7 +199,7 @@ private Set constructSitemapEvents(Item item, State state, List 0; } - if (!skipWidget || definesVisibilityOrColor(w, item.getName())) { + if (!skipWidget || definesVisibilityOrColorOrIcon(w, item.getName())) { SitemapWidgetEvent event = constructSitemapEventForWidget(item, state, w); events.add(event); } @@ -197,6 +213,9 @@ private SitemapWidgetEvent constructSitemapEventForWidget(Item item, State state event.pageId = pageId; event.label = itemUIRegistry.getLabel(widget); event.widgetId = itemUIRegistry.getWidgetId(widget); + event.icon = itemUIRegistry.getCategory(widget); + event.staticIcon = widget.getStaticIcon() != null || !widget.getIconRules().isEmpty(); + 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 @@ -237,11 +256,16 @@ 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.getIconRules().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name)); + } + + private boolean conditionsDependsOnItem(@Nullable EList conditions, String name) { + return conditions != null && conditions.stream().anyMatch(c -> name.equals(c.getItem())); } public void sitemapContentChanged(EList widgets) { 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 1b73d4222da..f229ed906bc 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 @@ -80,7 +80,9 @@ import org.openhab.core.model.sitemap.sitemap.Buttongrid; 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; @@ -135,6 +137,7 @@ * @author Mark Herwege - Added pattern and unit fields * @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 - New widget icon parameter based on conditional rules */ @Component(service = { RESTResource.class, EventSubscriber.class }) @JaxrsResource @@ -527,7 +530,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.getIconRules().isEmpty(); bean.labelcolor = convertItemValueColor(itemUIRegistry.getLabelColor(widget), itemState); bean.valuecolor = convertItemValueColor(itemUIRegistry.getValueColor(widget), itemState); bean.iconcolor = convertItemValueColor(itemUIRegistry.getIconColor(widget), itemState); @@ -757,6 +760,8 @@ private Set getAllItems(EList widgets) { 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())); @@ -802,6 +807,32 @@ private Set getItemsInColorCond(EList colorList) { return items; } + private Set getItemsInIconCond(EList ruleList) { + Set items = new HashSet<>(); + for (IconRule rule : ruleList) { + getItemsInConditions(rule.getConditions(), items); + } + return items; + } + + private void getItemsInConditions(@Nullable EList conditions, Set items) { + if (conditions != null) { + for (Condition condition : conditions) { + String itemName = condition.getItem(); + if (itemName != null) { + try { + Item item = itemUIRegistry.getItem(itemName); + if (item instanceof GenericItem genericItem) { + items.add(genericItem); + } + } catch (ItemNotFoundException e) { + // ignore + } + } + } + } + } + @Override public Set getSubscribedEventTypes() { return Set.of(ItemStateChangedEvent.TYPE); diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapWidgetEvent.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapWidgetEvent.java index a2ba2536d88..afb52cb6ea6 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapWidgetEvent.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapWidgetEvent.java @@ -19,6 +19,7 @@ * * @author Kai Kreuzer - Initial contribution * @author Laurent Garnier - New field iconcolor + * @author Laurent Garnier - New fields staticIcon and reloadIcon */ public class SitemapWidgetEvent extends SitemapEvent { @@ -26,6 +27,13 @@ public class SitemapWidgetEvent extends SitemapEvent { 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 boolean reloadIcon; public String labelcolor; public String valuecolor; public String iconcolor; 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 b5fe7d2f0c5..43d7b605f2d 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 @@ -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; diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java b/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java index b8a0b0da6ab..f48683aa9f9 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java @@ -49,6 +49,8 @@ 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; @@ -61,6 +63,7 @@ * 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) @@ -79,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; @@ -92,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; @@ -120,6 +130,7 @@ public void setup() throws Exception { 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); @@ -251,6 +262,30 @@ public void whenLongPollingShouldObserveItemsFromValueColorConditions() { // 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)); @@ -269,13 +304,15 @@ public void whenGetPageDataShouldReturnPageBean() throws ItemNotFoundException { 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)); @@ -286,10 +323,24 @@ public void whenGetPageDataShouldReturnPageBean() throws ItemNotFoundException { 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 { @@ -299,9 +350,10 @@ private void configureItemUIRegistry(State state1, State state2) throws ItemNotF 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"); @@ -310,13 +362,22 @@ private void configureItemUIRegistry(State state1, State state2) throws ItemNotF 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 initSitemapWidgets() { @@ -330,6 +391,19 @@ private EList initSitemapWidgets() { 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 conditions0 = new BasicEList<>(); + conditions0.add(conditon0); + when(iconRule.getConditions()).thenReturn(conditions0); + EList iconRules = new BasicEList<>(); + iconRules.add(iconRule); + when(w1.getIconRules()).thenReturn(iconRules); // add visibility rules to the mock widget: VisibilityRule visibilityRule = mock(VisibilityRule.class); @@ -359,6 +433,7 @@ private EList initSitemapWidgets() { iconColors.add(iconColor); when(w1.getIconColor()).thenReturn(iconColors); + iconRules = new BasicEList<>(); visibilityRules = new BasicEList<>(); labelColors = new BasicEList<>(); valueColors = new BasicEList<>(); @@ -371,14 +446,30 @@ private EList initSitemapWidgets() { 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); - BasicEList 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 widgets = new BasicEList<>(3); widgets.add(w1); widgets.add(w2); + widgets.add(w3); return widgets; } diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext index f4716e46008..b50799baebb 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext @@ -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)* ']'))? & @@ -194,6 +226,12 @@ ColorArray: ((item=ID)? (condition=('==' | '>' | '<' | '>=' | '<=' | '!='))? (sign=('-' | '+'))? (state=XState) '=')? (arg=STRING); +IconRule: + ((conditions+=Condition ('AND' conditions+=Condition)*) '=')? (arg=Icon); + +Condition: + (item=ID)? (condition=('==' | '>' | '<' | '>=' | '<=' | '!='))? (sign=('-' | '+'))? (state=XState); + Command returns ecore::EString: Number | ID | STRING; diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java index 2f59e2ed531..424c7e2ae37 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java @@ -32,6 +32,7 @@ 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; @@ -44,9 +45,11 @@ import org.openhab.core.model.sitemap.sitemap.impl.ChartImpl; import org.openhab.core.model.sitemap.sitemap.impl.ColorArrayImpl; import org.openhab.core.model.sitemap.sitemap.impl.ColorpickerImpl; +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 +98,7 @@ public class UIComponentSitemapProvider implements SitemapProvider, RegistryChan .compile("(?[A-Za-z]\\w*)\\s*(?==|!=|<=|>=|<|>)\\s*(?\\+|-)?(?.+)"); private static final Pattern COLOR_PATTERN = Pattern.compile( "((?[A-Za-z]\\w*)?\\s*((?==|!=|<=|>=|<|>)\\s*(?\\+|-)?(?[^=]*[^= ]+))?\\s*=)?\\s*(?\\S+)"); + private static final Pattern ICON_PATTERN = COLOR_PATTERN; private Map sitemaps = new HashMap<>(); private @Nullable UIComponentRegistryFactory componentRegistryFactory; @@ -296,6 +300,7 @@ protected Sitemap buildSitemap(RootUIComponent rootComponent) { addLabelColor(widget.getLabelColor(), component); addValueColor(widget.getValueColor(), component); addIconColor(widget.getIconColor(), component); + addIconRules(widget.getIconRules(), component); } return widget; @@ -426,6 +431,29 @@ private void addColor(EList color, UIComponent component, String key } } + private void addIconRules(EList 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); 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 c290fb52640..4a2e4f18119 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 @@ -68,6 +68,7 @@ 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; @@ -109,6 +110,7 @@ * @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 + * @author Laurent Garnier - new icon parameter based on conditional rules */ @NonNullByDefault @Component(immediate = true, configurationPid = "org.openhab.sitemap", // @@ -638,11 +640,14 @@ private String transform(String label, boolean matchTransform, @Nullable String // 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(); @@ -800,6 +805,7 @@ private void copyProperties(Widget source, Widget target) { 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())); } /** @@ -1295,6 +1301,86 @@ public boolean getVisiblity(Widget w) { return false; } + @Override + public @Nullable String getConditionalIcon(Widget w) { + List 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 conditions, + Widget w) { + boolean allConditionsOk = true; + if (conditions != null) { + State defaultState = getState(w); + + // Go through all AND conditions + for (org.openhab.core.model.sitemap.sitemap.Condition condition : conditions) { + // Use a local state variable in case it gets overridden below + State state = defaultState; + + // If there's an item defined here, get its state + String itemName = condition.getItem(); + if (itemName != null) { + // Try and find the item to test. + Item item; + try { + item = itemRegistry.getItem(itemName); + + // Get the item state + state = item.getState(); + } catch (ItemNotFoundException e) { + logger.warn("Cannot retrieve item {} for widget {}", itemName, + w.eClass().getInstanceTypeName()); + } + } + + // Handle the sign + String value; + if (condition.getSign() != null) { + value = condition.getSign() + condition.getState(); + } else { + value = condition.getState(); + } + + if (state == null || !matchStateToValue(state, value, condition.getCondition())) { + allConditionsOk = false; + break; + } + } + } + return allConditionsOk; + } + enum Condition { EQUAL("=="), GTE(">="), 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 a979543d4cf..81026e1ad64 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 @@ -35,6 +35,7 @@ * @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 * diff --git a/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java b/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java index cd00015cac1..35ba92d0c42 100644 --- a/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java +++ b/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java @@ -27,6 +27,7 @@ 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; @@ -60,7 +61,9 @@ import org.openhab.core.library.types.StringType; 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; @@ -83,6 +86,7 @@ /** * @author Kai Kreuzer - Initial contribution + * @author Laurent Garnier - Tests added for getCategory */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -1015,4 +1019,114 @@ public void getIconColor() { color = uiRegistry.getIconColor(widgetMock); assertEquals("blue", color); } + + @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 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 rules = new BasicEList<>(); + rules.add(rule); + BasicEList 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); + } }