Skip to content

Commit

Permalink
[Sitemap] Add optional conditional rules for icon
Browse files Browse the repository at this point in the history
This allows dynamic icons based on items states even with non OH icon sources.

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

Related to openhab/openhab-webui#1938

Signed-off-by: Laurent Garnier <[email protected]>
  • Loading branch information
lolodomo committed Oct 2, 2023
1 parent dda021a commit 70e3b19
Show file tree
Hide file tree
Showing 9 changed files with 453 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -119,6 +123,10 @@ private Set<Item> getAllItems(EList<Widget> 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());
Expand All @@ -140,6 +148,14 @@ private Set<Item> getAllItems(EList<Widget> widgets) {
return items;
}

private void addItemsFromConditions(Set<Item> items, @Nullable EList<Condition> conditions) {
if (conditions != null) {
for (Condition condition : conditions) {
addItemWithName(items, condition.getItem());
}
}
}

private void addItemWithName(Set<Item> items, String itemName) {
if (itemName != null) {
try {
Expand Down Expand Up @@ -183,7 +199,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 +213,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 +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<Condition> conditions, String name) {
return conditions != null && conditions.stream().anyMatch(c -> name.equals(c.getItem()));
}

public void sitemapContentChanged(EList<Widget> widgets) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -757,6 +760,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.getIconRules()));
// 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 @@ -802,6 +807,32 @@ private Set<GenericItem> getItemsInColorCond(EList<ColorArray> colorList) {
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) {
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<String> getSubscribedEventTypes() {
return Set.of(ItemStateChangedEvent.TYPE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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));
Expand All @@ -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));
Expand All @@ -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 {
Expand All @@ -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");
Expand All @@ -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<Widget> initSitemapWidgets() {
Expand All @@ -330,6 +391,19 @@ private EList<Widget> 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<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);
Expand Down Expand Up @@ -359,6 +433,7 @@ private EList<Widget> initSitemapWidgets() {
iconColors.add(iconColor);
when(w1.getIconColor()).thenReturn(iconColors);

iconRules = new BasicEList<>();
visibilityRules = new BasicEList<>();
labelColors = new BasicEList<>();
valueColors = new BasicEList<>();
Expand All @@ -371,14 +446,30 @@ private EList<Widget> 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<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;
}

Expand Down
Loading

0 comments on commit 70e3b19

Please sign in to comment.