4444import org .openhab .core .items .ItemNotFoundException ;
4545import org .openhab .core .items .ItemNotUniqueException ;
4646import org .openhab .core .items .ItemRegistry ;
47+ import org .openhab .core .items .Metadata ;
48+ import org .openhab .core .items .MetadataKey ;
49+ import org .openhab .core .items .MetadataRegistry ;
4750import org .openhab .core .library .items .CallItem ;
4851import org .openhab .core .library .items .ColorItem ;
4952import org .openhab .core .library .items .ContactItem ;
@@ -128,12 +131,17 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
128131 private static final int MAX_BUTTONS = 4 ;
129132
130133 private static final String DEFAULT_SORTING = "NONE" ;
134+ protected static final String WIDGET_ORDER_KEY = "widgetOrder" ;
135+ protected static final String SEMANTICS_KEY = "semantics" ;
136+ protected static final String SEMANTICS_LOCATION = "Location" ;
137+ protected static final String SEMANTICS_PARENT_LOCATION_CONFIG = "isPartOf" ;
131138
132139 private final Logger logger = LoggerFactory .getLogger (ItemUIRegistryImpl .class );
133140
134141 protected final Set <ItemUIProvider > itemUIProviders = new HashSet <>();
135142
136143 private final ItemRegistry itemRegistry ;
144+ private final MetadataRegistry metadataRegistry ;
137145 private final SitemapFactory sitemapFactory ;
138146 private final TimeZoneProvider timeZoneProvider ;
139147
@@ -153,8 +161,10 @@ public WidgetLabelWithSource(String l, WidgetLabelSource s) {
153161
154162 @ Activate
155163 public ItemUIRegistryImpl (final @ Reference ItemRegistry itemRegistry ,
156- final @ Reference SitemapFactory sitemapFactory , final @ Reference TimeZoneProvider timeZoneProvider ) {
164+ final @ Reference MetadataRegistry metadataRegistry , final @ Reference SitemapFactory sitemapFactory ,
165+ final @ Reference TimeZoneProvider timeZoneProvider ) {
157166 this .itemRegistry = itemRegistry ;
167+ this .metadataRegistry = metadataRegistry ;
158168 this .sitemapFactory = sitemapFactory ;
159169 this .timeZoneProvider = timeZoneProvider ;
160170 }
@@ -828,19 +838,14 @@ private List<Widget> getDynamicGroupChildren(Group group) {
828838 List <Item > members = new ArrayList <>(groupItem .getMembers ());
829839 switch (groupMembersSorting ) {
830840 case "LABEL" :
831- members .sort ((u1 , u2 ) -> {
832- String u1Label = u1 .getLabel ();
833- String u2Label = u2 .getLabel ();
834- if (u1Label != null && u2Label != null ) {
835- return u1Label .compareTo (u2Label );
836- } else {
837- return u1 .getName ().compareTo (u2 .getName ());
838- }
839- });
841+ members .sort (Comparator .comparing (this ::getItemLabel ));
840842 break ;
841843 case "NAME" :
842844 members .sort (Comparator .comparing (Item ::getName ));
843845 break ;
846+ case "METADATA" :
847+ sortByMetadata (members );
848+ break ;
844849 default :
845850 break ;
846851 }
@@ -865,6 +870,101 @@ private List<Widget> getDynamicGroupChildren(Group group) {
865870 return children ;
866871 }
867872
873+ private void sortByMetadata (List <Item > members ) {
874+ // Location items are sorted among themselves (by hierarchy, then widgetOrder),
875+ // and placed ahead of all other members.
876+ Map <Boolean , List <Item >> byLocation = members .stream ().collect (Collectors .partitioningBy (this ::isLocationItem ));
877+ List <Item > sorted = new ArrayList <>(byLocation .getOrDefault (true , List .of ()).stream ()
878+ .sorted (Comparator .comparing (Item ::getName , this ::compareLocationMembers )).toList ());
879+ sorted .addAll (byLocation .getOrDefault (false , List .of ()).stream ()
880+ .sorted (Comparator .comparing (Item ::getName , this ::compareWidgetOrder )).toList ());
881+ members .clear ();
882+ members .addAll (sorted );
883+ }
884+
885+ private boolean isLocationItem (Item item ) {
886+ MetadataKey semanticsKey = new MetadataKey (SEMANTICS_KEY , item .getName ());
887+ Metadata semantics = metadataRegistry .get (semanticsKey );
888+ return semantics != null && semantics .getValue ().startsWith (SEMANTICS_LOCATION );
889+ }
890+
891+ private String getItemLabel (Item item ) {
892+ String label = item .getLabel ();
893+ return label != null ? label : item .getName ();
894+ }
895+
896+ private String getItemLabel (String itemName ) {
897+ Item item = get (itemName );
898+ return item != null ? getItemLabel (item ) : itemName ;
899+ }
900+
901+ private @ Nullable String getParentLocationName (String locationItemName ) {
902+ MetadataKey semanticsKey = new MetadataKey (SEMANTICS_KEY , locationItemName );
903+ Metadata semantics = metadataRegistry .get (semanticsKey );
904+ Object isPartOf = semantics != null ? semantics .getConfiguration ().get (SEMANTICS_PARENT_LOCATION_CONFIG ) : null ;
905+ return isPartOf instanceof String ? (String ) isPartOf : null ;
906+ }
907+
908+ private int compareLocationMembers (String u1Name , String u2Name ) {
909+ List <String > u1Ancestors = new ArrayList <>();
910+ u1Ancestors .add (u1Name );
911+ String u1ParentName = getParentLocationName (u1Name );
912+ while (u1ParentName != null && !u1Ancestors .contains (u1ParentName )) {
913+ u1Ancestors .add (u1ParentName );
914+ u1ParentName = getParentLocationName (u1ParentName );
915+ }
916+ List <String > u2Ancestors = new ArrayList <>();
917+ u2Ancestors .add (u2Name );
918+ String u2ParentName = getParentLocationName (u2Name );
919+ while (u2ParentName != null && !u2Ancestors .contains (u2ParentName )) {
920+ u2Ancestors .add (u2ParentName );
921+ u2ParentName = getParentLocationName (u2ParentName );
922+ }
923+
924+ int u1Depth = u1Ancestors .size ();
925+ int u2Depth = u2Ancestors .size ();
926+ int minDepth = Math .min (u1Depth , u2Depth );
927+ for (int d = 0 ; d < minDepth ; d ++) {
928+ String ancestorName1 = u1Ancestors .get (u1Depth - 1 - d );
929+ String ancestorName2 = u2Ancestors .get (u2Depth - 1 - d );
930+ if (!ancestorName1 .equals (ancestorName2 )) {
931+ return compareWidgetOrder (ancestorName1 , ancestorName2 );
932+ }
933+ }
934+ // A parent comes before its children
935+ return Integer .compare (u1Depth , u2Depth );
936+ }
937+
938+ private int compareWidgetOrder (String u1Name , String u2Name ) {
939+ MetadataKey u1Key = new MetadataKey (WIDGET_ORDER_KEY , u1Name );
940+ Metadata u1Order = metadataRegistry .get (u1Key );
941+ MetadataKey u2Key = new MetadataKey (WIDGET_ORDER_KEY , u2Name );
942+ Metadata u2Order = metadataRegistry .get (u2Key );
943+ Float u1OrderValue = null ;
944+ try {
945+ u1OrderValue = u1Order != null ? Float .parseFloat (u1Order .getValue ()) : null ;
946+ } catch (NumberFormatException e ) {
947+ // ignore
948+ }
949+ Float u2OrderValue = null ;
950+ try {
951+ u2OrderValue = u2Order != null ? Float .parseFloat (u2Order .getValue ()) : null ;
952+ } catch (NumberFormatException e ) {
953+ // ignore
954+ }
955+
956+ if ((u1OrderValue == null && u2OrderValue == null )
957+ || (u1OrderValue != null && u1OrderValue .equals (u2OrderValue ))) {
958+ return getItemLabel (u1Name ).compareTo (getItemLabel (u2Name ));
959+ } else if (u1OrderValue == null ) {
960+ return 1 ;
961+ } else if (u2OrderValue == null ) {
962+ return -1 ;
963+ } else {
964+ return u1OrderValue .compareTo (u2OrderValue );
965+ }
966+ }
967+
868968 private boolean isReadOnly (String itemName ) {
869969 try {
870970 Item item = getItem (itemName );
0 commit comments