Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ab09698
nested sitemaps
mherwege May 6, 2026
46523e4
support setting linked sitemap name through item
mherwege May 8, 2026
560e9c0
nested sitemap widget is non-linkable
mherwege May 8, 2026
02c37f9
fix validation, shorten YAML widget type
mherwege May 8, 2026
a9bcdc7
yaml and yaml tests
mherwege May 8, 2026
f033cb5
fix
mherwege May 8, 2026
4d8129d
improve nested sitemap type
mherwege May 8, 2026
1f1638c
review feedback
mherwege May 12, 2026
424fb16
tests nested sitemaps in ItemUIRegistry
mherwege May 12, 2026
5a0ddbe
copilot feedback
mherwege May 13, 2026
f80ff5f
fix test
mherwege May 13, 2026
98a5bab
handle sitemap changes
mherwege May 13, 2026
8ff6305
copilot feedback
mherwege May 13, 2026
b2a3ba2
fix getting widget with illegal id
mherwege May 13, 2026
b6b207d
copilot review adjustments
mherwege May 13, 2026
d5e5d35
copilot review improvements
mherwege May 13, 2026
e71134a
fix tests
mherwege May 13, 2026
1d4cb91
remove from cache on sitemap change
mherwege May 13, 2026
215cc3a
rebase fix
mherwege May 13, 2026
da2d89a
fixes
mherwege May 13, 2026
5cc8db5
review feedback adjustments
mherwege May 15, 2026
81f7435
nested sitemap cache tests
mherwege May 15, 2026
8f9f4ea
fix DSL
mherwege May 15, 2026
3060443
review corrections
mherwege May 18, 2026
a8ca2f8
fix uiComponentSitemapMapper
mherwege May 18, 2026
2185408
merge
mherwege May 26, 2026
508dd89
fix merge error
mherwege May 26, 2026
c231b8c
fix merge issue
mherwege May 26, 2026
831a200
merge
mherwege May 27, 2026
14b41a2
use Sitemap constant
mherwege May 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,31 @@ SitemapModel:
{SitemapModel} (sitemaps += ModelSitemap)*;

ModelSitemap:
'sitemap' name=ID ('label=' label=STRING)? ('icon=' icon=STRING)? '{'
'sitemap' name=ID ('label=' label=STRING)? ('icon=' icon=Icon)? '{'
(children+=ModelWidget)*
'}';

ModelWidget:
(ModelLinkableWidget | ModelNonLinkableWidget);
ModelLinkableWidget | ModelNonLinkableWidget;

ModelNonLinkableWidget:
ModelSwitch | ModelSelection | ModelSlider | ModelSetpoint | ModelVideo | ModelChart | ModelWebview | ModelColorpicker | ModelColortemperaturepicker | ModelMapview | ModelInput | ModelButton | ModelDefault;
ModelSwitch | ModelSelection | ModelSlider | ModelSetpoint | ModelVideo | ModelChart | ModelWebview | ModelColorpicker | ModelColortemperaturepicker | ModelMapview | ModelInput | ModelButton | ModelNestedSitemap | ModelDefault;

ModelLinkableWidget:
(ModelText | ModelGroup | ModelImage | ModelFrame | ModelButtongrid)
('{'
(children+=ModelWidget)+
'}')?;

ModelNestedSitemap:
{ModelNestedSitemap} 'Sitemap' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) |
('icon=' icon=Icon) | ('icon=' IconRules=ModelIconRuleList) | ('staticIcon=' staticIcon=Icon) |
('name=' sitemapName=ID) |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

('labelcolor=' labelColor=ModelColorArrayList) |
('valuecolor=' valueColor=ModelColorArrayList) |
('iconcolor=' iconColor=ModelColorArrayList) |
('visibility=' visibility=ModelVisibilityRuleList))*;

ModelFrame:
{ModelFrame} 'Frame' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) |
('icon=' icon=Icon) | ('icon=' IconRules=ModelIconRuleList) | ('staticIcon=' staticIcon=Icon) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class SitemapFormatter extends AbstractDeclarativeFormatter {
c.setNoSpace().after("item=", "label=", "icon=", "staticIcon=")
c.setNoSpace().after("url=", "refresh=", "encoding=", "service=", "period=", "legend=", "forceasitem=", "yAxisDecimalPattern=", "interpolation=", "height=")
c.setNoSpace().after("minValue=", "maxValue=", "step=", "inputHint=", "row=", "column=", "click=", "release=")
c.setNoSpace().after("labelcolor=", "valuecolor=", "iconcolor=", "visibility=", "mappings=", "buttons=")
c.setNoSpace().after("labelcolor=", "valuecolor=", "iconcolor=", "visibility=", "mappings=", "buttons=", "name=")

c.setNoSpace().before(",")
c.setNoSpace().around(":", "=")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.openhab.core.model.sitemap.sitemap.ModelMapping;
import org.openhab.core.model.sitemap.sitemap.ModelMappingList;
import org.openhab.core.model.sitemap.sitemap.ModelMapview;
import org.openhab.core.model.sitemap.sitemap.ModelNestedSitemap;
import org.openhab.core.model.sitemap.sitemap.ModelSelection;
import org.openhab.core.model.sitemap.sitemap.ModelSetpoint;
import org.openhab.core.model.sitemap.sitemap.ModelSitemap;
Expand All @@ -72,6 +73,7 @@
import org.openhab.core.sitemap.LinkableWidget;
import org.openhab.core.sitemap.Mapping;
import org.openhab.core.sitemap.Mapview;
import org.openhab.core.sitemap.NestedSitemap;
import org.openhab.core.sitemap.Parent;
import org.openhab.core.sitemap.Rule;
import org.openhab.core.sitemap.Selection;
Expand All @@ -96,6 +98,7 @@
*
* @author Kai Kreuzer - Initial contribution
* @author Mark Herwege - Separate registry from model
* @author Mark Herwege - Add support for nested sitemaps
*/
@NonNullByDefault
@Component(service = { SitemapProvider.class, DslSitemapProvider.class }, immediate = true)
Expand Down Expand Up @@ -133,12 +136,11 @@ protected void deactivate() {
}

@Override
public @Nullable Sitemap getSitemap(String sitemapName) {
public @Nullable Sitemap getSitemap(String name) {
Sitemap sitemap = sitemapCache.entrySet().stream().filter(e -> !isIsolatedModel(e.getKey()))
.flatMap(e -> e.getValue().stream()).filter(s -> sitemapName.equals(s.getName())).findAny()
.orElse(null);
.flatMap(e -> e.getValue().stream()).filter(s -> name.equals(s.getName())).findAny().orElse(null);
if (sitemap == null) {
logger.trace("Sitemap {} cannot be found", sitemapName);
logger.trace("Sitemap {} cannot be found", name);
}
return sitemap;
}
Expand Down Expand Up @@ -251,6 +253,10 @@ private void addWidget(List<Widget> widgets, ModelWidget modelWidget, Parent par
ModelDefault modelDefault = (ModelDefault) modelWidget;
defaultWidget.setHeight(modelDefault.getHeight());
break;
case NestedSitemap nestedSitemapWidget:
ModelNestedSitemap modelNestedSitemap = (ModelNestedSitemap) modelWidget;
nestedSitemapWidget.setName(modelNestedSitemap.getSitemapName());
break;
default:
break;
}
Expand Down Expand Up @@ -287,6 +293,11 @@ private String getWidgetType(ModelWidget modelWidget) {
String instanceTypeName = modelWidget.eClass().getInstanceTypeName();
String widgetType = instanceTypeName
.substring(instanceTypeName.lastIndexOf("." + MODEL_TYPE_PREFIX) + MODEL_TYPE_PREFIX.length() + 1);
if (widgetType.equals("NestedSitemap")) {
// We need a different type for nested sitemaps, but can keep a common name for the model type, so we need
// to distinguish them here
widgetType = SitemapFactory.SITEMAP;
}
return widgetType;
}

Expand Down Expand Up @@ -316,8 +327,8 @@ private boolean addWidgetButtons(Buttongrid buttongridWidget, ModelButtongrid mo
"Defining buttons as properties of a Butongrid widget is deprecated although still supported; please prefer Button sub-widgets");
EList<ModelButtonDefinition> modelButtons = modelButtonList.getElements();
modelButtons.forEach(modelButton -> {
Button button = (Button) Objects.requireNonNull(sitemapFactory.createWidget(
org.openhab.core.sitemap.internal.registry.SitemapFactoryImpl.BUTTON, buttongridWidget));
Button button = (Button) Objects
.requireNonNull(sitemapFactory.createWidget(SitemapFactory.BUTTON, buttongridWidget));
button.setItem(modelButtongrid.getItem());
button.setRow(modelButton.getRow());
button.setColumn(modelButton.getColumn());
Expand Down Expand Up @@ -433,10 +444,9 @@ public void modelChanged(String modelName, EventType type) {
}

private void notifyRemovedSitemap(String modelName, Sitemap sitemap) {
String sitemapName = sitemap.getName();
String name = sitemap.getName();
Sitemap existingSitemap = sitemapCache.entrySet().stream().filter(e -> !e.getKey().equals(modelName))
.flatMap(e -> e.getValue().stream()).filter(s -> sitemapName.equals(s.getName())).findAny()
.orElse(null);
.flatMap(e -> e.getValue().stream()).filter(s -> name.equals(s.getName())).findAny().orElse(null);
if (existingSitemap != null) {
// Another sitemap with the same name exists, so we need to update it to use the other one instead
// of the removed one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.openhab.core.model.sitemap.sitemap.ModelMapping;
import org.openhab.core.model.sitemap.sitemap.ModelMappingList;
import org.openhab.core.model.sitemap.sitemap.ModelMapview;
import org.openhab.core.model.sitemap.sitemap.ModelNestedSitemap;
import org.openhab.core.model.sitemap.sitemap.ModelSelection;
import org.openhab.core.model.sitemap.sitemap.ModelSetpoint;
import org.openhab.core.model.sitemap.sitemap.ModelSitemap;
Expand Down Expand Up @@ -70,6 +71,7 @@
import org.openhab.core.sitemap.LinkableWidget;
import org.openhab.core.sitemap.Mapping;
import org.openhab.core.sitemap.Mapview;
import org.openhab.core.sitemap.NestedSitemap;
import org.openhab.core.sitemap.Rule;
import org.openhab.core.sitemap.Selection;
import org.openhab.core.sitemap.Setpoint;
Expand All @@ -93,6 +95,7 @@
* with the capabilities of parsing and generating file.
*
* @author Mark Herwege - Initial contribution
* @author Mark Herwege - Add support for nested sitemaps
*/
@NonNullByDefault
@Component(immediate = true, service = { SitemapSerializer.class, SitemapParser.class })
Expand Down Expand Up @@ -262,6 +265,11 @@ private ModelWidget buildModelWidget(Widget widget) {
modelChart.setInterpolation(chartWidget.getInterpolation());
modelWidget = modelChart;
}
case NestedSitemap nestedSitemapWidget -> {
ModelNestedSitemap modelNestedSitemap = SitemapFactory.eINSTANCE.createModelNestedSitemap();
modelNestedSitemap.setSitemapName(nestedSitemapWidget.getName());
modelWidget = modelNestedSitemap;
}
default -> {
ModelDefault modelDefault = SitemapFactory.eINSTANCE.createModelDefault();
if (widget instanceof Default defaultWidget) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import org.openhab.core.model.sitemap.sitemap.ModelWebview
import org.openhab.core.model.sitemap.sitemap.ModelVideo
import org.openhab.core.model.sitemap.sitemap.ModelWidget
import org.openhab.core.model.sitemap.sitemap.SitemapPackage
import org.openhab.core.model.sitemap.sitemap.ModelNestedSitemap

/**
* Custom validation rules.
Expand All @@ -50,7 +51,7 @@ class SitemapValidator extends AbstractSitemapValidator {
"item=", "label=", "icon=", "staticIcon=", "labelcolor=", "valuecolor=", "iconcolor=", "visibility=",
"url=", "encoding=", "service=", "refresh=", "period=", "legend=", "forceasitem=", "yAxisDecimalPattern=",
"interpolation=", "mappings=", "height=", "switchSupport", "releaseOnly", "minValue=", "maxValue=", "step=",
"inputHint=", "buttons=", "row=", "column=", "stateless", "click=", "release="
"inputHint=", "buttons=", "row=", "column=", "stateless", "click=", "release=", "name="
}
val ALLOWED_HINTS = #{"text", "number", "date", "time", "datetime"}
val ALLOWED_INTERPOLATION = #{"linear", "step"}
Expand Down Expand Up @@ -110,7 +111,10 @@ class SitemapValidator extends AbstractSitemapValidator {

@Check
def void checkWidgetHasItem(ModelWidget w) {
if (!(w instanceof ModelFrame || w instanceof ModelText || w instanceof ModelImage || w instanceof ModelVideo || w instanceof ModelWebview || w instanceof ModelButtongrid) && w.item === null) {
if (w instanceof ModelFrame || w instanceof ModelText || w instanceof ModelImage || w instanceof ModelVideo || w instanceof ModelWebview || w instanceof ModelButtongrid || w instanceof ModelNestedSitemap) {
return
}
if (w.item === null) {
error(buildMsgWithLineNb(getWidgetType(w) + " widget doesn't have item defined", w, null, null),
SitemapPackage.Literals.MODEL_WIDGET.getEStructuralFeature(SitemapPackage.MODEL_WIDGET__ITEM))
}
Expand Down Expand Up @@ -360,6 +364,14 @@ class SitemapValidator extends AbstractSitemapValidator {
SitemapPackage.Literals.MODEL_WEBVIEW.getEStructuralFeature(SitemapPackage.MODEL_WEBVIEW__URL))
}
}

@Check
def void checkNestedSitemapParameters(ModelNestedSitemap w) {
if (w.item === null && w.sitemapName === null) {
error(buildMsgWithLineNb("Sitemap widget doesn't have item or name defined", w, SitemapPackage.Literals.MODEL_NESTED_SITEMAP__SITEMAP_NAME, SitemapPackage.Literals.MODEL_WIDGET__ITEM),
SitemapPackage.Literals.MODEL_NESTED_SITEMAP.getEStructuralFeature(SitemapPackage.MODEL_NESTED_SITEMAP__SITEMAP_NAME))
}
}

def private buildMsgWithLineNb(String msg, EObject object, EAttribute attribute1, EAttribute attribute2) {
var INode node1 = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ private void addToList(@Nullable List<@NonNull String> list, String value) {
result.command = partial.command;
result.releaseCommand = partial.releaseCommand;
result.stateless = partial.stateless;
result.name = partial.name;

if (partial.label != null) {
if (partial.label.isValueNode()) {
Expand Down Expand Up @@ -381,6 +382,7 @@ protected static class YamlPartialWidgetDTO {
public String releaseCommand;
public Boolean stateless;
public JsonNode visibility;
public String name;
public List<YamlPartialWidgetDTO> widgets;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.openhab.core.sitemap.LinkableWidget;
import org.openhab.core.sitemap.Mapping;
import org.openhab.core.sitemap.Mapview;
import org.openhab.core.sitemap.NestedSitemap;
import org.openhab.core.sitemap.Parent;
import org.openhab.core.sitemap.Rule;
import org.openhab.core.sitemap.Selection;
Expand All @@ -64,6 +65,7 @@
* These sitemaps are automatically exposed to the {@link SitemapRegistry}.
*
* @author Laurent Garnier - Initial contribution
* @author Mark Herwege - Add support for nested sitemaps
*/
@NonNullByDefault
@Component(immediate = true, service = { SitemapProvider.class, YamlSitemapProvider.class, YamlModelListener.class })
Expand Down Expand Up @@ -298,6 +300,9 @@ public void removedModel(String modelName, Collection<YamlSitemapDTO> elements)
case Default defaultWidget:
defaultWidget.setHeight(widgetDTO.height);
break;
case NestedSitemap nestedSitemapWidget:
nestedSitemapWidget.setName(widgetDTO.name);
break;
default:
break;
}
Expand Down Expand Up @@ -387,12 +392,11 @@ private void addRuleConditions(List<Condition> conditions, @Nullable List<YamlCo
}

@Override
public @Nullable Sitemap getSitemap(String sitemapName) {
public @Nullable Sitemap getSitemap(String name) {
Sitemap sitemap = sitemapsMap.entrySet().stream().filter(e -> !isIsolatedModel(e.getKey()))
.flatMap(e -> e.getValue().stream()).filter(s -> sitemapName.equals(s.getName())).findAny()
.orElse(null);
.flatMap(e -> e.getValue().stream()).filter(s -> name.equals(s.getName())).findAny().orElse(null);
if (sitemap == null) {
logger.trace("Sitemap {} cannot be found", sitemapName);
logger.trace("Sitemap {} cannot be found", name);
}
return sitemap;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/
package org.openhab.core.model.yaml.internal.sitemaps;

import static org.openhab.core.sitemap.internal.registry.SitemapFactoryImpl.*;
import static org.openhab.core.sitemap.registry.SitemapFactory.*;

import java.math.BigDecimal;
import java.util.ArrayList;
Expand All @@ -33,6 +33,7 @@
* This is a data transfer object that is used to serialize widgets.
*
* @author Laurent Garnier - Initial contribution
* @author Mark Herwege - Add support for nested sitemaps
*/
public class YamlWidgetDTO {

Expand Down Expand Up @@ -61,6 +62,7 @@ public class YamlWidgetDTO {
MANDATORY_FIELDS.put(VIDEO, Set.of("url"));
MANDATORY_FIELDS.put(MAPVIEW, Set.of("item"));
MANDATORY_FIELDS.put(WEBVIEW, Set.of("url"));
MANDATORY_FIELDS.put(SITEMAP, Set.of());
MANDATORY_FIELDS.put(DEFAULT, Set.of("item"));

OPTIONAL_FIELDS.put(FRAME, Set.of("item"));
Expand All @@ -81,6 +83,7 @@ public class YamlWidgetDTO {
OPTIONAL_FIELDS.put(VIDEO, Set.of("item", "encoding"));
OPTIONAL_FIELDS.put(MAPVIEW, Set.of("height"));
OPTIONAL_FIELDS.put(WEBVIEW, Set.of("item", "height"));
OPTIONAL_FIELDS.put(SITEMAP, Set.of("item", "name"));
OPTIONAL_FIELDS.put(DEFAULT, Set.of("height"));
}

Expand Down Expand Up @@ -113,6 +116,7 @@ public class YamlWidgetDTO {
public String command;
public String releaseCommand;
public Boolean stateless;
public String name; // for NestedSitemap

public Object visibility;

Expand Down Expand Up @@ -164,6 +168,7 @@ public boolean isValid(@NonNull List<@NonNull String> errors, @NonNull List<@Non
ok &= isValidField("command", command, errors, warnings);
ok &= isValidField("releaseCommand", releaseCommand, errors, warnings);
ok &= isValidField("stateless", stateless, errors, warnings);
ok &= isValidField("name", name, errors, warnings);

if ((SWITCH.equals(type) || SELECTION.equals(type)) && mappings != null && !mappings.isEmpty()) {
for (YamlMappingDTO mapping : mappings) {
Expand All @@ -174,6 +179,9 @@ public boolean isValid(@NonNull List<@NonNull String> errors, @NonNull List<@Non
&& minValue.doubleValue() > maxValue.doubleValue()) {
addToList(warnings, "larger value %f for \"min\" field than value %f for \"max\" field"
.formatted(minValue.doubleValue(), maxValue.doubleValue()));
} else if (SITEMAP.equals(type) && (name == null || name.isBlank()) && (item == null || item.isBlank())) {
addToList(errors, "\"name\" or \"item\" field missing while mandatory for Sitemap widget");
ok = false;
}

List<String> ruleErrors = new ArrayList<>();
Expand Down Expand Up @@ -354,7 +362,7 @@ private void addToList(@Nullable List<@NonNull String> list, String value) {
public int hashCode() {
return Objects.hash(type, item, label, icon, mappings, switchSupport, releaseOnly, height, min, max, step, hint,
url, refresh, encoding, service, period, legend, forceAsItem, yAxisDecimalPattern, interpolation, row,
column, command, releaseCommand, stateless, visibility, widgets);
column, command, releaseCommand, stateless, visibility, widgets, name);
}

@Override
Expand All @@ -378,7 +386,8 @@ public boolean equals(@Nullable Object obj) {
&& Objects.equals(interpolation, other.interpolation) && Objects.equals(row, other.row)
&& Objects.equals(column, other.column) && Objects.equals(command, other.command)
&& Objects.equals(releaseCommand, other.releaseCommand) && Objects.equals(stateless, other.stateless)
&& Objects.equals(visibility, other.visibility) && Objects.equals(widgets, other.widgets);
&& Objects.equals(visibility, other.visibility) && Objects.equals(widgets, other.widgets)
&& Objects.equals(name, other.name);
}

record ButtonPosition(Integer row, Integer column) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.openhab.core.sitemap.LinkableWidget;
import org.openhab.core.sitemap.Mapping;
import org.openhab.core.sitemap.Mapview;
import org.openhab.core.sitemap.NestedSitemap;
import org.openhab.core.sitemap.Rule;
import org.openhab.core.sitemap.Selection;
import org.openhab.core.sitemap.Setpoint;
Expand All @@ -60,6 +61,7 @@
* {@link YamlSitemapConverter} is the YAML converter for {@link Sitemap} objects.
*
* @author Laurent Garnier - Initial contribution
* @author Mark Herwege - Add support for nested sitemaps
*/
@NonNullByDefault
@Component(immediate = true, service = { SitemapSerializer.class, SitemapParser.class })
Expand Down Expand Up @@ -304,6 +306,9 @@ private YamlWidgetDTO buildWidgetDTO(Widget widget) {
dto.height = defaultWidget.getHeight();
}
}
case NestedSitemap nestedSitemapWidget -> {
dto.name = nestedSitemapWidget.getName();
}
default -> {
}
}
Expand Down
Loading
Loading