Sonos grouping rules and UI visibility (bidirectional)

For a while, I have been thinking about how to replicate the grouping functionality in the Sonos application to OpenHab UI. Previously, I had simple rules that either was based on scenarios or predetermined grouping rules (add/remove A to B etc).

The basic idea was to be able to dynamically add or remove each player from a group (Group 1, Group 2 and Group 3) and use this to both display and control the specific group i the OH app. In addition, I also wanted the OH app to reflect the correct grouping when I control the Sonos system from the native application. I.e. if I group Bathroom with Kitchen in the Sonos app, I wanted the grouping in the OH app to reflect this.

Iā€™ve put quite a bit of work in to this, and after a period of testing, I think it works well enough to post it here so that others might enjoy the fruits of my labor. I am very sure a lot of the code is far from perfect, so feel free to post any optimization ideas.

Screenshots

The 4 players are grouped in two groups.

When pushing ā€œGroup 2ā€ the control panel for that group is shown. The volume controls will change based on which players are in the specific group. The other controls are those of the local coordinator in the group.

When one or more of the players are not grouped (i.e. standalone), individual control of the single player is shown.

Items

The items is a complete list of the channels available in the Sonos binding, but not all of them ares used in this example. In addition i have three other items that holds various states:

SonosGroupID_Kitc holds the ID of the group (here, the kitchen player).
Sonos_Group1_Number holds the number of players in a group.
Sonos_Group1_Visibility tells the UI if the group controls should be visible and which player is the local coordinator in that group.

The icons can be downloaded here: https://www.dropbox.com/sh/uualcu06oiiq21a/AADhj9uUh5Xbu8IeUbIHKDC3a?dl=0

Group gSonos

Group gSonosKitc (gSonos)
Group gSonosGues (gSonos)
Group gSonosBath (gSonos)
Group gSonosLivi (gSonos)

Group gSonosVolu "Volume"  <soundvolume>
Group gSonosMute "Mute"    <sonos_mute>
Group gSonosStop "Stop"    <sonos_stop> {autoupdate="false"}

String SonosGroupID_Kitc	"Kitchen group [%s]"
String SonosGroupID_Gues	"Guest room group [%s]"
String SonosGroupID_Bath	"Bathroom group [%s]"
String SonosGroupID_Livi	"Living room group [%s]"

Number Sonos_Group1_Number "Players in group 1 [%.1f]"
Number Sonos_Group2_Number "Players in group 2 [%.1f]"
Number Sonos_Group3_Number "Players in group 3 [%.1f]"

String Sonos_Group1_Visibility "Group 1 visibility [%s]"
String Sonos_Group2_Visibility "Group 2 visibility [%s]"
String Sonos_Group3_Visibility "Group 3 visibility [%s]"


/* Kitchen */
String Sonos_Kitc_Add                     "Add [%s]"                  <sonos_add>          (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:add"}
Switch Sonos_Kitc_SetAlarm                "Set Alarm"                 <sonos_alarm>        (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:alarm"}
String Sonos_Kitc_AlarmProperties         "Alarm Properties [%s]"     <sonos_alarm_prop>   (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:alarmproperties"}
Switch Sonos_Kitc_AlarmIsrunning          "Alarm is running"          <sonso_alarm_run>    (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:alarmrunning"}
Player Sonos_Kitc_Control                 "Control"                   <sonos_control>      (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:control"}
String Sonos_Kitc_CurrentAlbum            "Album [%s]"                <sonos_album>        (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:currentalbum"}
String Sonos_Kitc_CurrentArtist           "Artist [%s]"               <sonos_artist>       (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:currentartist"}
String Sonos_Kitc_CurrentTitle            "Title [%s]"                <sonos_title>        (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:currenttitle"}
String Sonos_Kitc_CurrentTrack            "Track [%s]"                <sonos_track>        (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:currenttrack"}
Switch Sonos_Kitc_Shuffle                 "Shuffle"                   <sonos_shuffle>      (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:shuffle"}
String Sonos_Kitc_Repeat                  "Repeat [%s]"               <sonos_repeat>       (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:repeat"}
String Sonos_Kitc_Favorite                "Favorite [%s]"             <sonos_favorite>     (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:favorite"}
Switch Sonos_Kitc_Led                     "Led"                       <sonos_led>          (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:led"}
Switch Sonos_Kitc_LocalCoordinator        "Local Coordinator"         <sonos_coordinator>  (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:localcoordinator"}
Switch Sonos_Kitc_Mute                    "Mute"                      <sonos_mute>         (gSonosKitc,gSonosMute) {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:mute"}
String Sonos_Kitc_NotificationSound       "Notification Sound [%s]"   <sonos_notification> (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:notificationsound"}
Dimmer Sonos_Kitc_Notificationsoundvolume "Notification Sound Volume" <soundvolume>        (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:notificationvolume"}
String Sonos_Kitc_PlayPlaylist            "Play Playlist [%s]"        <sonos_playlist>     (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:playlist"}
Switch Sonos_Kitc_PlayQueue               "Play Queue"                <sonos_que>          (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:playqueue"}
Number Sonos_Kitc_PlayTrack               "Play Track"                <sonos_play_track>   (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:playtrack"}
String Sonos_Kitc_PlayURI                 "Play URI [%s]"             <sonos_play_uri>     (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:playuri"}
Switch Sonos_Kitc_PublicAddress           "Public Address"            <sonos_publiv>       (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:publicaddress"}
String Sonos_Kitc_Radio                   "Radio [%s]"                <sonos_radio>        (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:radio"}
String Sonos_Kitc_Remove                  "Remove [%s]"               <sonos_remove>       (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:remove"}
Switch Sonos_Kitc_Restore                 "Restore"                   <sonos_restore>      (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:restore"}
Switch Sonos_Kitc_RestoreAll              "Restore All"               <sonos_restore_all>  (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:restoreall"}
Switch Sonos_Kitc_Save                    "Save"                      <sonos_save>         (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:save"}
Switch Sonos_Kitc_SaveAll                 "Save All"                  <sonos_save_all>     (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:saveall"}
Number Sonos_Kitc_Snooze                  "Snooze"                    <sonos_snooze>       (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:snooze"}
Switch Sonos_Kitc_StandAlone              "Stand Alone"               <sonos_stand_alone>  (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:standalone"}
String Sonos_Kitc_State                   "State [%s]"                <sonos_state>        (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:state"}
Switch Sonos_Kitc_Stop                    "Stop"                      <sonos_stop>         (gSonosKitc,gSonosStop) {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:stop", autoupdate="false"}
Dimmer Sonos_Kitc_Volume                  "Kitchen"                   <soundvolume>        (gSonosKitc,gSonosVolu) {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:volume"}
String Sonos_Kitc_ZoneGroupID             "Zone Group ID [%s]"        <sonos_zone>         (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:zonegroupid"}
String Sonos_Kitc_ZoneName                "Zone Name [%s]"            <sonos_zone>         (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:zonename"}
String Sonos_Kitc_Coordinator             "Coordinator [%s]"          <sonos_coordinator>  (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:coordinator"}
Number Sonos_Kitc_SleepTimer              "Sleep Timer"               <sonos_sleep_timer>  (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:sleeptimer"}
String Sonos_Kitc_CurrentAVtransportURI   "AV transport URI [%s]"     <sonos_uri>          (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:currenttransporturi"}
String Sonos_Kitc_CurrenttrackURI         "track URI [%s]"            <sonos_uri>          (gSonosKitc)            {channel="sonos:PLAY1:RINCON_XXXXXXXXXXXXXXXXX:currenttrackuri"}

Rules

I have only posted one rule per player, but this is easily extended by duplicating the rules to the number of players needed. The two bits of information that the rules relies heavily on, is the playerā€™s zonegroupid and localcoordinator. Actions performed on a group of players, needs to be done on the player that is the local coordinator. I have added some comments to the code, but expect some effort to be invested if you plan to fully understand what is going on.

Since functions only allow a maximum of 6 parameters to be passed, this setup can handle a maximum of 3 separate groups.

import org.openhab.core.library.types.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*
import org.java.math.*
import org.joda.time.*

var Timer CheckSinglePlayerTimer = null
var Timer SonosGroupVisibility = null

val Functions$Function6 updateSingleGroups = [
		NumberItem group1_num,
		NumberItem group2_num,
		NumberItem group3_num,
		StringItem player_a_groupID,
		StringItem player_b_groupID,
		StringItem player_a_name |

		// If the player the updated player has been grouped to is in the "NONE"-group (or is NULL),
		// find a group with zero players and update status of both players.
		if(player_b_groupID.state=="0" || player_b_groupID.state==NULL) {
			if(group1_num.state==0) {
				postUpdate(player_a_groupID,"1")
				postUpdate(player_b_groupID,"1")
				logInfo("openhab", player_a_name + " player + 1 updated to group 1")

			} else if(group2_num.state==0) {
				postUpdate(player_a_groupID,"2")
				postUpdate(player_b_groupID,"2")
				logInfo("openhab", player_a_name + " player + 1 updated to group 2")

			} else if(group3_num.state==0) {
				postUpdate(player_a_groupID,"3")
				postUpdate(player_b_groupID,"3")
				logInfo("openhab", player_a_name + " player + 1 updated to group 3")
			}
		// Else set the updated player to the group id of the matched player
		} else if(player_a_groupID.state!=player_b_groupID.state) {
			postUpdate(player_a_groupID,player_b_groupID.state)
			logInfo("openhab", player_a_name + " player updated to group " + player_b_groupID.state.toString)
		}
		return NULL
	]

val Functions$Function6 checkSinglePlayers = [
		StringItem CheckZonegroupID,
		StringItem ZonegroupID_a,
		StringItem ZonegroupID_b,
		StringItem ZonegroupID_c,
		StringItem UpdatePlayer,
		StringItem UpdatePlayerName |

		if(CheckZonegroupID.state!=ZonegroupID_a.state && CheckZonegroupID.state!=ZonegroupID_b.state && CheckZonegroupID.state!=ZonegroupID_c.state) {
			postUpdate(UpdatePlayer,"0")
			logInfo("openhab", UpdatePlayerName + "player updated to group 0")

		}
		return NULL
	]

///// NUMBER OF PLAYERS IN GROUP
//
//	A simple count of the number of players
//	in each group is made. This is used in
//	various rules, e.g. to determine
//	if a group should be visible in the UI or not.
//
/////////////////////////////////

rule "Sonos number of players in group"
	when
		Item SonosGroupID_Kitc received update or
		Item SonosGroupID_Gues received update or
		Item SonosGroupID_Livi received update or
		Item SonosGroupID_Bath received update
	then
		var Number Group_1 = 0
		var Number Group_2 = 0
		var Number Group_3 = 0

		if(SonosGroupID_Kitc.state=="1"){
			Group_1 = Group_1 + 1
		}else if(SonosGroupID_Kitc.state=="2"){
			Group_2 = Group_2 + 1
		}else if(SonosGroupID_Kitc.state=="3"){
			Group_3 = Group_3 + 1
		}

		if(SonosGroupID_Gues.state=="1"){
			Group_1 = Group_1 + 1
		}else if(SonosGroupID_Gues.state=="2"){
			Group_2 = Group_2 + 1
		}else if(SonosGroupID_Gues.state=="3"){
			Group_3 = Group_3 + 1
		}

		if(SonosGroupID_Bath.state=="1"){
			Group_1 = Group_1 + 1
		}else if(SonosGroupID_Bath.state=="2"){
			Group_2 = Group_2 + 1
		}else if(SonosGroupID_Bath.state=="3"){
			Group_3 = Group_3 + 1
		}

		if(SonosGroupID_Livi.state=="1"){
			Group_1 = Group_1 + 1
		}else if(SonosGroupID_Livi.state=="2"){
			Group_2 = Group_2 + 1
		}else if(SonosGroupID_Livi.state=="3"){
			Group_3 = Group_3 + 1
		}

		postUpdate(Sonos_Group1_Number,Group_1)
		postUpdate(Sonos_Group2_Number,Group_2)
		postUpdate(Sonos_Group3_Number,Group_3)
end

///// GROUPING RULES
//
//	The rule will check every other player that the
//	  (1) Group number of the player beeing added corresponds to the player it is beeing added to
//	  (2) Zone-ids are not equal (not already grouped)
//	  (3) That the player beeing checked has a state (not NULL)
//
//	Additional nested logic is required to determine in what direction the player shall be grouped (A to B or B to A).
//	This is a special case that kicks in when both players are single before grouping: It is not possible
//	to add a player that is the local coordinator to a player which is not.
//
/////////////////////////////////


rule "Sonos group kitchen"
	when 
		Item SonosGroupID_Kitc received command
	then
	
		// Short sleep to make shure the rule to calculate the number of players in a group has executed before the logic is applied.
		Thread::sleep(300)

		if(SonosGroupID_Kitc.state==SonosGroupID_Gues.state && SonosGroupID_Kitc.state!="0" && Sonos_Kitc_ZoneGroupID.state!=Sonos_Gues_ZoneGroupID.state && Sonos_Gues_ZoneGroupID.state!=NULL && ((Sonos_Gues_LocalCoordinator.state==ON) || (Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))))){ 
			
			logInfo("openhab","Entered sonos kitchen grouping rule number 1")
			
			if(Sonos_Gues_LocalCoordinator.state==ON){
				Sonos_Gues_Add.sendCommand("RINCON_kitchenplayer")
				logInfo("openhab","Kitchen player added to guest room")
			}else if(Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))) {
				Sonos_Kitc_Add.sendCommand("RINCON_guestroomplayer")
				logInfo("openhab","Guest room player added to kitchen")
			}

		}else if(SonosGroupID_Kitc.state==SonosGroupID_Bath.state && SonosGroupID_Kitc.state!="0" && Sonos_Kitc_ZoneGroupID.state!=Sonos_Bath_ZoneGroupID.state && Sonos_Bath_ZoneGroupID.state!=NULL && ((Sonos_Bath_LocalCoordinator.state==ON) || (Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))))){ 
			
			logInfo("openhab","Entered sonos kitchen grouping rule number 2")
			
			if(Sonos_Bath_LocalCoordinator.state==ON){
				Sonos_Bath_Add.sendCommand("RINCON_kitchenplayer")
				logInfo("openhab","Kitchen player added to bathroom")
			}else if(Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))) {
				Sonos_Kitc_Add.sendCommand("RINCON_bathroomplayer")
				logInfo("openhab","Bathroom player added to kitchen")
			}

		}else if(SonosGroupID_Kitc.state==SonosGroupID_Livi.state && SonosGroupID_Kitc.state!="0" && Sonos_Kitc_ZoneGroupID.state!=Sonos_Livi_ZoneGroupID.state && Sonos_Livi_ZoneGroupID.state!=NULL && ((Sonos_Livi_LocalCoordinator.state==ON) || (Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))))){ 
			
			logInfo("openhab","Entered sonos kitchen grouping rule number 3")
			
			if(Sonos_Livi_LocalCoordinator.state==ON){
				Sonos_Livi_Add.sendCommand("RINCON_kitchenplayer")
				logInfo("openhab","Kitchen player added to living room")
			}else if(Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))) {
				Sonos_Kitc_Add.sendCommand("RINCON_livingroomplayer")
				logInfo("openhab","Living room player added to Kitchen")
			}

		}else{
			Sonos_Kitc_StandAlone.sendCommand("ON")
			logInfo("openhab","Kitchen player set to stand alone")
		}
end

//// UPDATE GROUPING RULES
//
//	The rules will update the appropiate group
//	when a sonos player is grouped, either
//  in the OH2 UI or within the Sonos app.
//	
/////////////////////////////////

rule "Sonos update group membership bathroom"
	when
		Item Sonos_Bath_ZoneGroupID changed
	then
		if(Sonos_Bath_ZoneGroupID.state==Sonos_Kitc_ZoneGroupID.state && Sonos_Kitc_ZoneGroupID.state!=NULL) {
			updateSingleGroups.apply(Sonos_Group1_Number, Sonos_Group2_Number, Sonos_Group3_Number, SonosGroupID_Bath, SonosGroupID_Kitc, "Bathroom")

		} else if(Sonos_Bath_ZoneGroupID.state==Sonos_Gues_ZoneGroupID.state && Sonos_Gues_ZoneGroupID.state!=NULL) {
			updateSingleGroups.apply(Sonos_Group1_Number, Sonos_Group2_Number, Sonos_Group3_Number, SonosGroupID_Bath, SonosGroupID_Gues, "Bathroom")

		} else if(Sonos_Bath_ZoneGroupID.state==Sonos_Livi_ZoneGroupID.state && Sonos_Livi_ZoneGroupID.state!=NULL) {
			updateSingleGroups.apply(Sonos_Group1_Number, Sonos_Group2_Number, Sonos_Group3_Number, SonosGroupID_Bath, SonosGroupID_Livi, "Bathroom")

		// The zone-id changed, but is not grouped with any other players (removed from group).
		// To allow grouping in the UI without the state being reset to single, a timer is applied.
		// This works since grouping in both the OH UI and the Sonos app usually is not performed simultaneously.
		} else {
			if(CheckSinglePlayerTimer==null) {
				CheckSinglePlayerTimer = createTimer(now.plusSeconds(20), [|
					checkSinglePlayers.apply(Sonos_Bath_ZoneGroupID, Sonos_Kitc_ZoneGroupID, Sonos_Gues_ZoneGroupID, Sonos_Livi_ZoneGroupID, SonosGroupID_Bath,"Bathroom")
					checkSinglePlayers.apply(Sonos_Kitc_ZoneGroupID, Sonos_Bath_ZoneGroupID, Sonos_Gues_ZoneGroupID, Sonos_Livi_ZoneGroupID, SonosGroupID_Kitc,"Kitchen")
					checkSinglePlayers.apply(Sonos_Gues_ZoneGroupID, Sonos_Bath_ZoneGroupID, Sonos_Kitc_ZoneGroupID, Sonos_Livi_ZoneGroupID, SonosGroupID_Gues,"Guest room")
					checkSinglePlayers.apply(Sonos_Livi_ZoneGroupID, Sonos_Bath_ZoneGroupID, Sonos_Kitc_ZoneGroupID, Sonos_Gues_ZoneGroupID, SonosGroupID_Livi,"Living room")
					CheckSinglePlayerTimer = null
				])
				logInfo("openhab","CheckSinglePlayerTimer scheduled")
			} else {
				CheckSinglePlayerTimer.reschedule(now.plusSeconds(20))
				logInfo("openhab","CheckSinglePlayerTimer rescheduled")
			}
		} 

end

///// GROUP VISIBILITY
//
//	The rule is used to control the UI.
//  Specifically, it used to determin
//  if a group should be hidden and if
//  no, which player is to be shown within
//  the group. This has to be the local
//	coordinator in the group.
// 
/////////////////////////////////

rule "Sonos group visibility"
	when
		Item SonosGroupID_Kitc received update or
		Item SonosGroupID_Gues received update or
		Item SonosGroupID_Bath received update or
		Item SonosGroupID_Livi received update 
	then
		if(SonosGroupVisibility==null) {
			SonosGroupVisibility = createTimer(now.plusSeconds(5), [|

				if(Sonos_Group1_Number.state<=1){
					postUpdate(Sonos_Group1_Visibility,"Hide")

				}else if(Sonos_Group1_Number.state>=2){
					if(SonosGroupID_Kitc.state=="1" && Sonos_Kitc_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group1_Visibility,"Kitchen")
					}else if(SonosGroupID_Gues.state=="1" && Sonos_Gues_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group1_Visibility,"Guest room")
					}else if(SonosGroupID_Bath.state=="1" && Sonos_Bath_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group1_Visibility,"Bathroom")
					}else if(SonosGroupID_Livi.state=="1" && Sonos_Livi_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group1_Visibility,"Living room")
					}
				}

				if(Sonos_Group2_Number.state<=1){
					postUpdate(Sonos_Group2_Visibility,"Hide")
				}else if(Sonos_Group2_Number.state>=2){
					if(SonosGroupID_Kitc.state=="2" && Sonos_Kitc_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group2_Visibility,"Kitchen")
					}else if(SonosGroupID_Gues.state=="2" && Sonos_Gues_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group2_Visibility,"Guest room")
					}else if(SonosGroupID_Bath.state=="2" && Sonos_Bath_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group2_Visibility,"Bathroom")
					}else if(SonosGroupID_Livi.state=="2" && Sonos_Livi_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group2_Visibility,"Living room")
					}
				}

				if(Sonos_Group3_Number.state<=1){
					postUpdate(Sonos_Group3_Visibility,"Hide")
				}else if(Sonos_Group3_Number.state>=2){
					if(SonosGroupID_Kitc.state=="3" && Sonos_Kitc_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group3_Visibility,"Kitchen")
					}else if(SonosGroupID_Gues.state=="3" && Sonos_Gues_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group3_Visibility,"Guest room")
					}else if(SonosGroupID_Bath.state=="3" && Sonos_Bath_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group3_Visibility,"Bathroom")
					}else if(SonosGroupID_Livi.state=="3" && Sonos_Livi_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group3_Visibility,"Living room")
					}
				}

			])
			logInfo("openhab","SonosGroupVisibility scheduled")
			SonosGroupVisibility = null

		} else {
			SonosGroupVisibility.reschedule(now.plusSeconds(5))
			logInfo("openhab","SonosGroupVisibility rescheduled")
		}

end

(continued in next post)

9 Likes

(continuedā€¦)

Sitemap

The sitemap should be pretty straightforward to configure using the visibility variable, but Iā€™ll post it for reference. Note that there is some formatting issued that I havenā€™t been able to solve yet.

sitemap sonos label="Sonos" {

	Frame label="Sonos" {

		Slider  item=gSonosVolu label="Volume all"
		Switch  item=gSonosStop label="Stop all"	mappings=[OFF="Stop"]
		Switch  item=gSonosMute label="Mute all"	mappings=[ON="Mute", OFF="Unmute"]

	}

	Frame visibility=[Sonos_Group1_Visibility!="Hide"]{
		Text label="Group 1"
		icon="sonos_control" {

			Frame visibility=[Sonos_Group1_Visibility=="Kitchen"]{
				Default item=Sonos_Kitc_Control 
			}
			Frame visibility=[Sonos_Group1_Visibility=="Guest room"]{
				Default item=Sonos_Gues_Control
			}
			Frame visibility=[Sonos_Group1_Visibility=="Bathroom"]{
				Default item=Sonos_Bath_Control
			}
			Frame visibility=[Sonos_Group1_Visibility=="Living room"]{
				Default item=Sonos_Livi_Control
			}

			Slider item=Sonos_Kitc_Volume visibility=[SonosGroupID_Kitc=="1"]
			Slider item=Sonos_Gues_Volume visibility=[SonosGroupID_Gues=="1"]
			Slider item=Sonos_Bath_Volume visibility=[SonosGroupID_Bath=="1"]
			Slider item=Sonos_Livi_Volume visibility=[SonosGroupID_Livi=="1"]

			Switch item=Sonos_Kitc_Mute visibility=[Sonos_Group1_Visibility=="Kitchen"] 
			Switch item=Sonos_Gues_Mute visibility=[Sonos_Group1_Visibility=="Guest room"]
			Switch item=Sonos_Bath_Mute visibility=[Sonos_Group1_Visibility=="Bathroom"] 
			Switch item=Sonos_Livi_Mute visibility=[Sonos_Group1_Visibility=="Living room"]

			Switch item=Sonos_Kitc_Shuffle visibility=[Sonos_Group1_Visibility=="Kitchen"] 
			Switch item=Sonos_Gues_Shuffle visibility=[Sonos_Group1_Visibility=="Guest room"]
			Switch item=Sonos_Bath_Shuffle visibility=[Sonos_Group1_Visibility=="Bathroom"] 
			Switch item=Sonos_Livi_Shuffle visibility=[Sonos_Group1_Visibility=="Living room"]

			Switch item=Sonos_Kitc_Repeat visibility=[Sonos_Group1_Visibility=="Kitchen"]     mappings=[ALL="All", ONE="One", OFF="Off"]
			Switch item=Sonos_Gues_Repeat visibility=[Sonos_Group1_Visibility=="Guest room"]  mappings=[ALL="All", ONE="One", OFF="Off"]
			Switch item=Sonos_Bath_Repeat visibility=[Sonos_Group1_Visibility=="Bathroom"]    mappings=[ALL="All", ONE="One", OFF="Off"]
			Switch item=Sonos_Livi_Repeat visibility=[Sonos_Group1_Visibility=="Living room"] mappings=[ALL="All", ONE="One", OFF="Off"]

			Selection item=Sonos_RadioStation_Kitc_Number visibility=[Sonos_Group1_Visibility=="Kitchen"]     mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_RadioStation_Gues_Number visibility=[Sonos_Group1_Visibility=="Guest room"]  mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_RadioStation_Bath_Number visibility=[Sonos_Group1_Visibility=="Bathroom"]    mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_RadioStation_Livi_Number visibility=[Sonos_Group1_Visibility=="Living room"] mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]

			Selection item=Sonos_Playlist_Kitc_Number visibility=[Sonos_Group1_Visibility=="Kitchen"]     mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Selection item=Sonos_Playlist_Gues_Number visibility=[Sonos_Group1_Visibility=="Guest room"]  mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Selection item=Sonos_Playlist_Bath_Number visibility=[Sonos_Group1_Visibility=="Bathroom"]    mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Selection item=Sonos_Playlist_Livi_Number visibility=[Sonos_Group1_Visibility=="Living room"] mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]

			Text item=Sonos_Kitc_CurrentArtist visibility=[Sonos_Group1_Visibility=="Kitchen"]
			Text item=Sonos_Gues_CurrentArtist visibility=[Sonos_Group1_Visibility=="Guest room"]
			Text item=Sonos_Bath_CurrentArtist visibility=[Sonos_Group1_Visibility=="Bathroom"]
			Text item=Sonos_Livi_CurrentArtist visibility=[Sonos_Group1_Visibility=="Living room"]

			Text item=Sonos_Kitc_CurrentAlbum visibility=[Sonos_Group1_Visibility=="Kitchen"]
			Text item=Sonos_Gues_CurrentAlbum visibility=[Sonos_Group1_Visibility=="Guest room"]
			Text item=Sonos_Bath_CurrentAlbum visibility=[Sonos_Group1_Visibility=="Bathroom"]
			Text item=Sonos_Livi_CurrentAlbum visibility=[Sonos_Group1_Visibility=="Living room"]

			Text item=Sonos_Kitc_CurrentTitle visibility=[Sonos_Group1_Visibility=="Kitchen"]
			Text item=Sonos_Gues_CurrentTitle visibility=[Sonos_Group1_Visibility=="Guest room"]
			Text item=Sonos_Bath_CurrentTitle visibility=[Sonos_Group1_Visibility=="Bathroom"]
			Text item=Sonos_Livi_CurrentTitle visibility=[Sonos_Group1_Visibility=="Living room"]
		}
	}

	Frame visibility=[Sonos_Group2_Visibility!="Hide"]{
		Text label="Group 2"
		icon="sonos_control" {
			Frame visibility=[Sonos_Group2_Visibility=="Kitchen"]{
				Default item=Sonos_Kitc_Control 
			}
			Frame visibility=[Sonos_Group2_Visibility=="Guest room"]{
				Default item=Sonos_Gues_Control
			}
			Frame visibility=[Sonos_Group2_Visibility=="Bathroom"]{
				Default item=Sonos_Bath_Control
			}
			Frame visibility=[Sonos_Group2_Visibility=="Living room"]{
				Default item=Sonos_Livi_Control
			}
			Slider item=Sonos_Kitc_Volume visibility=[SonosGroupID_Kitc=="2"]
			Slider item=Sonos_Gues_Volume visibility=[SonosGroupID_Gues=="2"]
			Slider item=Sonos_Bath_Volume visibility=[SonosGroupID_Bath=="2"]
			Slider item=Sonos_Livi_Volume visibility=[SonosGroupID_Livi=="2"]

			Switch item=Sonos_Kitc_Mute visibility=[Sonos_Group1_Visibility=="Kitchen"] 
			Switch item=Sonos_Gues_Mute visibility=[Sonos_Group1_Visibility=="Guest room"]
			Switch item=Sonos_Bath_Mute visibility=[Sonos_Group1_Visibility=="Bathroom"] 
			Switch item=Sonos_Livi_Mute visibility=[Sonos_Group1_Visibility=="Living room"]

			Switch item=Sonos_Kitc_Shuffle visibility=[Sonos_Group2_Visibility=="Kitchen"] 
			Switch item=Sonos_Gues_Shuffle visibility=[Sonos_Group2_Visibility=="Guest room"]
			Switch item=Sonos_Bath_Shuffle visibility=[Sonos_Group2_Visibility=="Bathroom"] 
			Switch item=Sonos_Livi_Shuffle visibility=[Sonos_Group2_Visibility=="Living room"]

			Switch item=Sonos_Kitc_Repeat visibility=[Sonos_Group2_Visibility=="Kitchen"]     mappings=[ALL="All", ONE="One", OFF="Off"]
			Switch item=Sonos_Gues_Repeat visibility=[Sonos_Group2_Visibility=="Guest room"]  mappings=[ALL="All", ONE="One", OFF="Off"]
			Switch item=Sonos_Bath_Repeat visibility=[Sonos_Group2_Visibility=="Bathroom"]    mappings=[ALL="All", ONE="One", OFF="Off"]
			Switch item=Sonos_Livi_Repeat visibility=[Sonos_Group2_Visibility=="Living room"] mappings=[ALL="All", ONE="One", OFF="Off"]

			Selection item=Sonos_RadioStation_Kitc_Number visibility=[Sonos_Group2_Visibility=="Kitchen"]     mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_RadioStation_Gues_Number visibility=[Sonos_Group2_Visibility=="Guest room"]  mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_RadioStation_Bath_Number visibility=[Sonos_Group2_Visibility=="Bathroom"]    mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_RadioStation_Livi_Number visibility=[Sonos_Group2_Visibility=="Living room"] mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]

			Selection item=Sonos_Playlist_Kitc_Number visibility=[Sonos_Group2_Visibility=="Kitchen"]     mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Selection item=Sonos_Playlist_Gues_Number visibility=[Sonos_Group2_Visibility=="Guest room"]  mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Selection item=Sonos_Playlist_Bath_Number visibility=[Sonos_Group2_Visibility=="Bathroom"]    mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Selection item=Sonos_Playlist_Livi_Number visibility=[Sonos_Group2_Visibility=="Living room"] mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]

			Text item=Sonos_Kitc_CurrentArtist visibility=[Sonos_Group2_Visibility=="Kitchen"]
			Text item=Sonos_Gues_CurrentArtist visibility=[Sonos_Group2_Visibility=="Guest room"]
			Text item=Sonos_Bath_CurrentArtist visibility=[Sonos_Group2_Visibility=="Bathroom"]
			Text item=Sonos_Livi_CurrentArtist visibility=[Sonos_Group2_Visibility=="Living room"]

			Text item=Sonos_Kitc_CurrentAlbum visibility=[Sonos_Group2_Visibility=="Kitchen"]
			Text item=Sonos_Gues_CurrentAlbum visibility=[Sonos_Group2_Visibility=="Guest room"]
			Text item=Sonos_Bath_CurrentAlbum visibility=[Sonos_Group2_Visibility=="Bathroom"]
			Text item=Sonos_Livi_CurrentAlbum visibility=[Sonos_Group2_Visibility=="Living room"]

			Text item=Sonos_Kitc_CurrentTitle visibility=[Sonos_Group2_Visibility=="Kitchen"]
			Text item=Sonos_Gues_CurrentTitle visibility=[Sonos_Group2_Visibility=="Guest room"]
			Text item=Sonos_Bath_CurrentTitle visibility=[Sonos_Group2_Visibility=="Bathroom"]
			Text item=Sonos_Livi_CurrentTitle visibility=[Sonos_Group2_Visibility=="Living room"]
		}
	}

	Frame visibility=[Sonos_Group3_Visibility!="Hide"]{
		Text label="Group 3"
		icon="sonos_control" {
			Frame visibility=[Sonos_Group3_Visibility=="Kitchen"]{
				Default item=Sonos_Kitc_Control 
			}
			Frame visibility=[Sonos_Group3_Visibility=="Guest room"]{
				Default item=Sonos_Gues_Control
			}
			Frame visibility=[Sonos_Group3_Visibility=="Bathroom"]{
				Default item=Sonos_Bath_Control
			}
			Frame visibility=[Sonos_Group3_Visibility=="Living room"]{
				Default item=Sonos_Livi_Control
			}
			Slider item=Sonos_Kitc_Volume visibility=[SonosGroupID_Kitc=="3"]
			Slider item=Sonos_Gues_Volume visibility=[SonosGroupID_Gues=="3"]
			Slider item=Sonos_Bath_Volume visibility=[SonosGroupID_Bath=="3"]
			Slider item=Sonos_Livi_Volume visibility=[SonosGroupID_Livi=="3"]

			Switch item=Sonos_Kitc_Mute visibility=[Sonos_Group1_Visibility=="Kitchen"] 
			Switch item=Sonos_Gues_Mute visibility=[Sonos_Group1_Visibility=="Guest room"]
			Switch item=Sonos_Bath_Mute visibility=[Sonos_Group1_Visibility=="Bathroom"] 
			Switch item=Sonos_Livi_Mute visibility=[Sonos_Group1_Visibility=="Living room"]

			Switch item=Sonos_Kitc_Shuffle visibility=[Sonos_Group3_Visibility=="Kitchen"] 
			Switch item=Sonos_Gues_Shuffle visibility=[Sonos_Group3_Visibility=="Guest room"]
			Switch item=Sonos_Bath_Shuffle visibility=[Sonos_Group3_Visibility=="Bathroom"] 
			Switch item=Sonos_Livi_Shuffle visibility=[Sonos_Group3_Visibility=="Living room"]

			Switch item=Sonos_Kitc_Repeat visibility=[Sonos_Group3_Visibility=="Kitchen"]     mappings=[ALL="All", ONE="One", OFF="Off"]
			Switch item=Sonos_Gues_Repeat visibility=[Sonos_Group3_Visibility=="Guest room"]  mappings=[ALL="All", ONE="One", OFF="Off"]
			Switch item=Sonos_Bath_Repeat visibility=[Sonos_Group3_Visibility=="Bathroom"]    mappings=[ALL="All", ONE="One", OFF="Off"]
			Switch item=Sonos_Livi_Repeat visibility=[Sonos_Group3_Visibility=="Living room"] mappings=[ALL="All", ONE="One", OFF="Off"]

			Selection item=Sonos_RadioStation_Kitc_Number visibility=[Sonos_Group3_Visibility=="Kitchen"]     mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_RadioStation_Gues_Number visibility=[Sonos_Group3_Visibility=="Guest room"]  mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_RadioStation_Bath_Number visibility=[Sonos_Group3_Visibility=="Bathroom"]    mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_RadioStation_Livi_Number visibility=[Sonos_Group3_Visibility=="Living room"] mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]

			Selection item=Sonos_Playlist_Kitc_Number visibility=[Sonos_Group3_Visibility=="Kitchen"]     mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Selection item=Sonos_Playlist_Gues_Number visibility=[Sonos_Group3_Visibility=="Guest room"]  mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Selection item=Sonos_Playlist_Bath_Number visibility=[Sonos_Group3_Visibility=="Bathroom"]    mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Selection item=Sonos_Playlist_Livi_Number visibility=[Sonos_Group3_Visibility=="Living room"] mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]

			Text item=Sonos_Kitc_CurrentArtist visibility=[Sonos_Group3_Visibility=="Kitchen"]
			Text item=Sonos_Gues_CurrentArtist visibility=[Sonos_Group3_Visibility=="Guest room"]
			Text item=Sonos_Bath_CurrentArtist visibility=[Sonos_Group3_Visibility=="Bathroom"]
			Text item=Sonos_Livi_CurrentArtist visibility=[Sonos_Group3_Visibility=="Living room"]

			Text item=Sonos_Kitc_CurrentAlbum visibility=[Sonos_Group3_Visibility=="Kitchen"]
			Text item=Sonos_Gues_CurrentAlbum visibility=[Sonos_Group3_Visibility=="Guest room"]
			Text item=Sonos_Bath_CurrentAlbum visibility=[Sonos_Group3_Visibility=="Bathroom"]
			Text item=Sonos_Livi_CurrentAlbum visibility=[Sonos_Group3_Visibility=="Living room"]

			Text item=Sonos_Kitc_CurrentTitle visibility=[Sonos_Group3_Visibility=="Kitchen"]
			Text item=Sonos_Gues_CurrentTitle visibility=[Sonos_Group3_Visibility=="Guest room"]
			Text item=Sonos_Bath_CurrentTitle visibility=[Sonos_Group3_Visibility=="Bathroom"]
			Text item=Sonos_Livi_CurrentTitle visibility=[Sonos_Group3_Visibility=="Living room"]
		}
	}

	Frame visibility=[SonosGroupID_Kitc=="0"]{
		Text label="Kitchen"
		icon="sonos_control" {
			Default item=Sonos_Kitc_Control 
			Slider item=Sonos_Kitc_Volume
			Switch item=Sonos_Kitc_Mute 
			Switch item=Sonos_Kitc_Shuffle 
			Switch item=Sonos_Kitc_Repeat mappings=[ALL="All", ONE="One", OFF="Off"]
			Selection item=Sonos_RadioStation_Kitc_Number mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_Playlist_Kitc_Number mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Text item=Sonos_Kitc_CurrentArtist
			Text item=Sonos_Kitc_CurrentAlbum
			Text item=Sonos_Kitc_CurrentTitle

		}
	}

	Frame visibility=[SonosGroupID_Gues=="0"]{
		Text label="Guest room"
		icon="sonos_control" {
			Default item=Sonos_Gues_Control 
			Slider item=Sonos_Gues_Volume
			Switch item=Sonos_Gues_Mute 
			Switch item=Sonos_Gues_Shuffle 
			Switch item=Sonos_Gues_Repeat mappings=[ALL="All", ONE="One", OFF="Off"]
			Selection item=Sonos_RadioStation_Gues_Number mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_Playlist_Gues_Number mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Text item=Sonos_Gues_CurrentArtist
			Text item=Sonos_Gues_CurrentAlbum
			Text item=Sonos_Gues_CurrentTitle

		}
	}

	Frame visibility=[SonosGroupID_Bath=="0"]{
		Text label="Bathroom"
		icon="sonos_control" {
			Default item=Sonos_Bath_Control 
			Slider item=Sonos_Bath_Volume
			Switch item=Sonos_Bath_Mute 
			Switch item=Sonos_Bath_Shuffle 
			Switch item=Sonos_Bath_Repeat mappings=[ALL="All", ONE="One", OFF="Off"]
			Selection item=Sonos_RadioStation_Bath_Number mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_Playlist_Bath_Number mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Text item=Sonos_Bath_CurrentArtist
			Text item=Sonos_Bath_CurrentAlbum
			Text item=Sonos_Bath_CurrentTitle

		}
	}

	Frame visibility=[SonosGroupID_Livi=="0"]{
		Text label="Living room"
		icon="sonos_control" {
			Default item=Sonos_Livi_Control 
			Slider item=Sonos_Livi_Volume
			Switch item=Sonos_Livi_Mute 
			Switch item=Sonos_Livi_Shuffle 
			Switch item=Sonos_Livi_Repeat mappings=[ALL="All", ONE="One", OFF="Off"]
			Selection item=Sonos_RadioStation_Livi_Number mappings=[0="None", 1="NRK P1 Ƙstlandssendingen", 2="NRK P2", 3="NRK P3", 4="NRK Alltid Nyheter", 5="NRK Klassisk", 6="NRK Jazz", 7="NRK MP3", 8="P4 Radio Norge", 9="Radio Norge"]
			Selection item=Sonos_Playlist_Livi_Number mappings=[0="None", 1="Peiskos", 2="Rolig jazz", 3="Rolig Musikk", 4="All Work And No Play", 5="Schlagere"]
			Text item=Sonos_Livi_CurrentArtist
			Text item=Sonos_Livi_CurrentAlbum
			Text item=Sonos_Livi_CurrentTitle

		}
	}

	Selection item=SonosGroupID_Bath icon="sonos_coordinator" mappings=[0=Single, 1="Group 1", 2="Group 2", 3="Group 3"]
	Selection item=SonosGroupID_Gues icon="sonos_coordinator" mappings=[0=Single, 1="Group 1", 2="Group 2", 3="Group 3"]
	Selection item=SonosGroupID_Kitc icon="sonos_coordinator" mappings=[0=Single, 1="Group 1", 2="Group 2", 3="Group 3"]
	Selection item=SonosGroupID_Livi icon="sonos_coordinator" mappings=[0=Single, 1="Group 1", 2="Group 2", 3="Group 3"]		

}
14 Likes

Iā€™m just at the beginning of openHAB and it is hard to understand the openHAB code sometimes.
I have still some problems with it but got this Sonos tutorial running after a while.

Thanks a lot for it!

Greetings,
Feit

Hello @torsteinsunde.

Thank you for this great example. Thatā€™s really something you cam up with there. Iā€™m currently in the progress of adapting your posted code to my home. I might get rid of the whole group functionality though. My speakers are always one group :wink:

So far I found two problems with your code (I believe). 1. val Number Group_1 = 0 etc need to be var definitions and 2. your Functions (Function6) are missing an explicit return value, confusing the SmartHome Designer. Adding one solves the problem (pay attention to the last line):

Thanks again :wink:

Thanks, @ThomDietrich! I have corrected the errors. I guess I should get in to the habit of checking my scripts with the designer IDE. Currently i only use Sublime Text 3 for coding, mostly due to its awesome multiline editing capabilities and wide range of available packages.

Iā€™m right there with you, but with the (more :stuck_out_tongue_winking_eye:) awesome Atom editor. Still I always check with the designer afterwards.

Could you give me a quick hint while we are at it!? I thought about it and I really do not need the grouping feature. My system is always one group. In fact Iā€™d like Sonos to recreate this one group by the click of a button. For me it would be more important to have sitemap elements to control the playback, unrelated to who is the current coordinator.
Can I define the coordinator or do we need to work with the one elected by sonos? Which parts of your rules are not needed for that case? I did only look over your code for a short time and will probably figure this out eventually, would be great if you gave me a boost :wink:

Hehe, we all have our preferences.

So I have actually implemented a ā€œgroup allā€ functionality my self. For me, the use case is broadcasting alarms and various scenarios. Since Iā€™m never sure of how the players are grouped, and I want to be 100 % that they are at the time I need them to, Iā€™m using this approach:

  1. Set player A to single.
  2. Group all players to player A.
  3. Control the group by issuing command to player A.

Since I start with player A (stand alone) and group all the others to it, I know that player A is the local coordinator of the group, and therefor the one the commands should be issued to.

Since this is a special use case, Iā€™ve actually made a separate function for this specific purpose. Since the function is issuing a lot of grouping commands, Iā€™ve added several timers (by trial and error) to allow the actual pairing in the Sonos system to catch up before issuing the next command. The function also allows to set the volume of each individual player, both the normal volume and the notification volume.

I hope this can be of some help.

val Functions$Function6 sonosGroupAllSetVolume = [
		String trigger,
		String mode,
		Number guesVolume,
		Number bathVolume,
		Number kitcVolume,
		Number liviVolume |

		var Timer SonosTimer_1 = null
		var Timer SonosTimer_2 = null
		var Timer SonosTimer_3 = null
		var Timer SonosTimer_4 = null
		var Timer SonosTimer_5 = null

		logInfo("openhab","Sonos group all and set volume command triggered by "  + trigger + ".")

		if(SonosTimer_1==null) {
			SonosTimer_1 = createTimer(now.plusSeconds(1), [|
				sendCommand(Sonos_Gues_StandAlone,ON)
				logInfo("openhab","Guest room player set to stand alone")
				SonosTimer_1 = null
			])
		}else {
			SonosTimer_1.reschedule(now.plusSeconds(1)
		}

		if(SonosTimer_2==null) {
			SonosTimer_2 = createTimer(now.plusSeconds(3), [|
				sendCommand(Sonos_Gues_Add,"RINCON_KITCHEN")
				logInfo("openhab","Kitchen player added to guest room")
				SonosTimer_2 = null
			])
		}else {
			SonosTimer_2.reschedule(now.plusSeconds(3)
		}

		if(SonosTimer_3==null) {
			SonosTimer_3 = createTimer(now.plusSeconds(5), [|
				sendCommand(Sonos_Gues_Add,"RINCON_GUESTROOM")
				logInfo("openhab","Bathroom player added to guest room")
				SonosTimer_3 = null
			])
		}else {
			SonosTimer_3.reschedule(now.plusSeconds(5)
		}

		if(SonosTimer_4==null) {
			SonosTimer_4 = createTimer(now.plusSeconds(7), [|
				sendCommand(Sonos_Gues_Add,"RINCON_LIVINGROOM")
				logInfo("openhab","Living room player added to guest room")
				SonosTimer_4 = null
			])
		}else {
			SonosTimer_4.reschedule(now.plusSeconds(7)
		}
		
		if(SonosTimer_5==null) {
			SonosTimer_5 = createTimer(now.plusSeconds(9), [|
				if(mode=="notification") {
					sendCommand(gSonosStop,ON)
					sendCommand(Sonos_Kitc_Notificationsoundvolume,kitcVolume)
					sendCommand(Sonos_Gues_Notificationsoundvolume,guesVolume)
					sendCommand(Sonos_Livi_Notificationsoundvolume,liviVolume)
					sendCommand(Sonos_Bath_Notificationsoundvolume,bathVolume)

				}else if(mode=="normal") {
					sendCommand(Sonos_Kitc_Volume,kitcVolume)
					sendCommand(Sonos_Gues_Volume,guesVolume)
					sendCommand(Sonos_Livi_Volume,liviVolume)
					sendCommand(Sonos_Bath_Volume,bathVolume)
				}

				SonosTimer_5 = null
			])

		}else {
			SonosTimer_5.reschedule(now.plusSeconds(9)
		}
		return NULL
]
1 Like

Thanks for the great effort!

However, having some small problems. Everything works for itself, but as soon as I group 2 players, I canā€™t get volume on 1 of them. Seems itā€™s only the top one in the group that I can control volume on, and there is no sound for player number 2. Any thoughts? Seems itā€™s controlling the right volume for each player and they are in the same group in the UI but only sound on 1ā€¦

To me, it sounds like the players arenā€™t actually grouped. You can check this by either opening up the Sonos controller app or check that the zonegroupid of the two players match in OpenHab.

If the players arenā€™t really grouped (on Sonos), this should have been handled by the checkSinglePlayers function. This function is triggered by the rule Sonos update group membership <room>. This specific rule has to be replicated for each for the players. In the example posted in this thread, I have only included this once - but my actual implementation, the rule is repeated 4 times (one for each player).

Hi Torstein,

That is the feeling I get as well - however the idea of checking the actual SONOS app was not something that crossed my mindā€¦ Iā€™ll give it a go tonight or tomorrow to see if your ideas solve the problem. I have in fact not replicated the rule as you mention above, and that might be the key. Iā€™ll try and let you know how it goes.

Have you by any chance made a Sonos widget, including the grouping function for HabPanel?

TJ

No, I havenā€™t. Itā€™s really something I would like to test out, but time is a constraint.

Hi again Torstein,

No luck to get it to group. Any chance you can share your whole rules file so I can try that one out?
Iā€™ve tried replicating the ā€œBathā€ rule and use it for the ā€œKitcā€ part, but I canā€™t get sound on one player so it doesnā€™t seem to group:(

-pƄ forhƄnd takk :slight_smile:

Check your inbox :slight_smile:

Hi Torstein,

great work with your sonos setup. I am currently struggeling with several settings but trying hard :wink: If you donā€™t mind your full code would help me a lot as a reference.
I am also trying to have special alarm sounds triggered e.g. dog barking by pressing a dash button which might require ungrouping before playing the uri. Unfortunately I am not a programmer and have to learn from code samples from others ā€œtrial and errorā€ but this works quite well (but time consuming).

I might also ask you some (dumb) questions in the future as I think you are sonos ./. openHAB expert already.

Thanks again for sharing so much.

Uwe

Hello @torsteinsunde

Thanks a lot for your example-code. I copied and tried to make it work. Unfortunately it donā€™t work for me. Like @tjwesterby I canā€™t get it to group.
I have errors in my openhab.log which Iā€™m not able to solve.

2018-05-11 13:57:23.119 [INFO ] [el.core.internal.ModelRepositoryImpl] - Loading model 'SonosZoneDemo.items'
2018-05-11 13:57:42.378 [INFO ] [el.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'SonosZoneDemo.rules', using it anyway:
The use of wildcard imports is deprecated.
The use of wildcard imports is deprecated.
The use of wildcard imports is deprecated.
The use of wildcard imports is deprecated.
The use of wildcard imports is deprecated.
Function6 is a raw type. References to generic type Function6<P1, P2, P3, P4, P5, P6, Result> should be parameterized
Function6 is a raw type. References to generic type Function6<P1, P2, P3, P4, P5, P6, Result> should be parameterized
The operator '==' should be replaced by '===' when null is one of the arguments.
The operator '==' should be replaced by '===' when null is one of the arguments.
2018-05-11 13:57:42.390 [INFO ] [el.core.internal.ModelRepositoryImpl] - Loading model 'SonosZoneDemo.rules'
2018-05-11 13:57:43.131 [INFO ] [thome.model.lsp.internal.ModelServer] - Started Language Server Protocol (LSP) service on port 5007
2018-05-11 13:57:43.936 [INFO ] [el.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'SonosZoneDemo.sitemap', using it anyway:
Sitemap should contain either only frames or none at all
2018-05-11 13:57:43.944 [INFO ] [el.core.internal.ModelRepositoryImpl] - Loading model 'SonosZoneDemo.sitemap'
2018-05-11 13:57:48.733 [INFO ] [basic.internal.servlet.WebAppServlet] - Started Basic UI at /basicui/app
2018-05-11 13:57:49.009 [INFO ] [arthome.ui.paper.internal.PaperUIApp] - Started Paper UI at /paperui
2018-05-11 13:57:49.186 [INFO ] [panel.internal.HABPanelDashboardTile] - Started HABPanel at /habpanel
2018-05-11 14:00:19.825 [INFO ] [lipse.smarthome.model.script.openhab] - SonosGroupVisibility scheduled
2018-05-11 14:00:19.851 [INFO ] [lipse.smarthome.model.script.openhab] - Entered sonos kitchen grouping rule number 2
2018-05-11 14:00:19.815 [ERROR] [model.script.actions.ScriptExecution] - Failed to schedule code for execution.
org.quartz.ObjectAlreadyExistsException: Unable to store Job : 'DEFAULT.2018-05-11T14:00:24.532+02:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ | {
  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@7e2c16
  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@110d570
  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@2514c4
} ]', because one already exists with this identification.
	at org.quartz.simpl.RAMJobStore.storeJob(RAMJobStore.java:279) ~[?:?]
	at org.quartz.simpl.RAMJobStore.storeJobAndTrigger(RAMJobStore.java:251) ~[?:?]
	at org.quartz.core.QuartzScheduler.scheduleJob(QuartzScheduler.java:886) ~[?:?]
	at org.quartz.impl.StdScheduler.scheduleJob(StdScheduler.java:249) ~[?:?]
	at org.eclipse.smarthome.model.script.actions.ScriptExecution.makeTimer(ScriptExecution.java:133) ~[?:?]
	at org.eclipse.smarthome.model.script.actions.ScriptExecution.createTimer(ScriptExecution.java:92) ~[?:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1085) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1060) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._invokeFeature(XbaseInterpreter.java:1046) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeFeature(XbaseInterpreter.java:991) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:143) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:901) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:864) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:223) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:1211) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:215) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:446) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:227) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:459) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:243) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:446) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:227) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:219) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:189) ~[?:?]
	at org.eclipse.smarthome.model.script.runtime.internal.engine.ScriptImpl.execute(ScriptImpl.java:82) ~[?:?]
	at org.eclipse.smarthome.model.rule.runtime.internal.engine.RuleEngineImpl.lambda$2(RuleEngineImpl.java:344) ~[?:?]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [?:?]
	at java.util.concurrent.FutureTask.run(FutureTask.java:266) [?:?]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [?:?]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [?:?]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:?]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:?]
	at java.lang.Thread.run(Thread.java:748) [?:?]
2018-05-11 14:00:19.948 [INFO ] [lipse.smarthome.model.script.openhab] - SonosGroupVisibility scheduled
2018-05-11 14:00:19.954 [INFO ] [lipse.smarthome.model.script.openhab] - Kitchen player added to bathroom
2018-05-11 14:00:29.316 [INFO ] [lipse.smarthome.model.script.openhab] - SonosGroupVisibility scheduled

Could you please share your whole code that I can try a new aproach?

Thanks!

Try this code, I have adapted this for OH 2.4.0. I have not tried the code myself yet. I still have to update Openhab to 2.5.0M3 ā€¦ because I have Ikea speakers in use :wink:

var Timer CheckSinglePlayerTimer = null
var Timer SonosGroupVisibility = null

val Functions$Function6<NumberItem,NumberItem,NumberItem,StringItem,StringItem,StringItem,Boolean> updateSingleGroups = [
		NumberItem group1_num,
		NumberItem group2_num,
		NumberItem group3_num,
		StringItem player_a_groupID,
		StringItem player_b_groupID,
		StringItem player_a_name |

		// If the player the updated player has been grouped to is in the "NONE"-group (or is NULL),
		// find a group with zero players and update status of both players.
		if(player_b_groupID.state=="0" || player_b_groupID.state===NULL) {
			if(group1_num.state==0) {
				postUpdate(player_a_groupID,"1")
				postUpdate(player_b_groupID,"1")
				logInfo("openhab", player_a_name + " player + 1 updated to group 1")

			} else if(group2_num.state==0) {
				postUpdate(player_a_groupID,"2")
				postUpdate(player_b_groupID,"2")
				logInfo("openhab", player_a_name + " player + 1 updated to group 2")

			} else if(group3_num.state==0) {
				postUpdate(player_a_groupID,"3")
				postUpdate(player_b_groupID,"3")
				logInfo("openhab", player_a_name + " player + 1 updated to group 3")
			}
		// Else set the updated player to the group id of the matched player
		} else if(player_a_groupID.state!=player_b_groupID.state) {
			postUpdate(player_a_groupID,player_b_groupID.state)
			logInfo("openhab", player_a_name + " player updated to group " + player_b_groupID.state.toString)
		}
		return NULL
	]

val Functions$Function6<StringItem,StringItem,StringItem,StringItem,StringItem,StringItem,Boolean> checkSinglePlayers = [
		StringItem CheckZonegroupID,
		StringItem ZonegroupID_a,
		StringItem ZonegroupID_b,
		StringItem ZonegroupID_c,
		StringItem UpdatePlayer,
		StringItem UpdatePlayerName |

		if(CheckZonegroupID.state!=ZonegroupID_a.state && CheckZonegroupID.state!=ZonegroupID_b.state && CheckZonegroupID.state!=ZonegroupID_c.state) {
			postUpdate(UpdatePlayer,"0")
			logInfo("openhab", UpdatePlayerName + "player updated to group 0")

		}
		return NULL
	]

///// NUMBER OF PLAYERS IN GROUP
//
//	A simple count of the number of players
//	in each group is made. This is used in
//	various rules, e.g. to determine
//	if a group should be visible in the UI or not.
//
/////////////////////////////////

rule "Sonos number of players in group"
	when
		Item SonosGroupID_Kitc received update or
		Item SonosGroupID_Gues received update or
		Item SonosGroupID_Livi received update or
		Item SonosGroupID_Bath received update
	then
		var Number Group_1 = 0
		var Number Group_2 = 0
		var Number Group_3 = 0

		if(SonosGroupID_Kitc.state=="1"){
			Group_1 = Group_1 + 1
		}else if(SonosGroupID_Kitc.state=="2"){
			Group_2 = Group_2 + 1
		}else if(SonosGroupID_Kitc.state=="3"){
			Group_3 = Group_3 + 1
		}

		if(SonosGroupID_Gues.state=="1"){
			Group_1 = Group_1 + 1
		}else if(SonosGroupID_Gues.state=="2"){
			Group_2 = Group_2 + 1
		}else if(SonosGroupID_Gues.state=="3"){
			Group_3 = Group_3 + 1
		}

		if(SonosGroupID_Bath.state=="1"){
			Group_1 = Group_1 + 1
		}else if(SonosGroupID_Bath.state=="2"){
			Group_2 = Group_2 + 1
		}else if(SonosGroupID_Bath.state=="3"){
			Group_3 = Group_3 + 1
		}

		if(SonosGroupID_Livi.state=="1"){
			Group_1 = Group_1 + 1
		}else if(SonosGroupID_Livi.state=="2"){
			Group_2 = Group_2 + 1
		}else if(SonosGroupID_Livi.state=="3"){
			Group_3 = Group_3 + 1
		}

		postUpdate(Sonos_Group1_Number,Group_1)
		postUpdate(Sonos_Group2_Number,Group_2)
		postUpdate(Sonos_Group3_Number,Group_3)
end

///// GROUPING RULES
//
//	The rule will check every other player that the
//	  (1) Group number of the player beeing added corresponds to the player it is beeing added to
//	  (2) Zone-ids are not equal (not already grouped)
//	  (3) That the player beeing checked has a state (not NULL)
//
//	Additional nested logic is required to determine in what direction the player shall be grouped (A to B or B to A).
//	This is a special case that kicks in when both players are single before grouping: It is not possible
//	to add a player that is the local coordinator to a player which is not.
//
/////////////////////////////////


rule "Sonos group kitchen"
	when 
		Item SonosGroupID_Kitc received command
	then
	
		// Short sleep to make shure the rule to calculate the number of players in a group has executed before the logic is applied.
		Thread::sleep(300)

		if(SonosGroupID_Kitc.state==SonosGroupID_Gues.state && SonosGroupID_Kitc.state!="0" && Sonos_Kitc_ZoneGroupID.state!=Sonos_Gues_ZoneGroupID.state && Sonos_Gues_ZoneGroupID.state!=NULL && ((Sonos_Gues_LocalCoordinator.state==ON) || (Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))))){ 
			
			logInfo("openhab","Entered sonos kitchen grouping rule number 1")
			
			if(Sonos_Gues_LocalCoordinator.state==ON){
				Sonos_Gues_Add.sendCommand("RINCON_kitchenplayer")
				logInfo("openhab","Kitchen player added to guest room")
			}else if(Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))) {
				Sonos_Kitc_Add.sendCommand("RINCON_guestroomplayer")
				logInfo("openhab","Guest room player added to kitchen")
			}

		}else if(SonosGroupID_Kitc.state==SonosGroupID_Bath.state && SonosGroupID_Kitc.state!="0" && Sonos_Kitc_ZoneGroupID.state!=Sonos_Bath_ZoneGroupID.state && Sonos_Bath_ZoneGroupID.state!=NULL && ((Sonos_Bath_LocalCoordinator.state==ON) || (Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))))){ 
			
			logInfo("openhab","Entered sonos kitchen grouping rule number 2")
			
			if(Sonos_Bath_LocalCoordinator.state==ON){
				Sonos_Bath_Add.sendCommand("RINCON_kitchenplayer")
				logInfo("openhab","Kitchen player added to bathroom")
			}else if(Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))) {
				Sonos_Kitc_Add.sendCommand("RINCON_bathroomplayer")
				logInfo("openhab","Bathroom player added to kitchen")
			}

		}else if(SonosGroupID_Kitc.state==SonosGroupID_Livi.state && SonosGroupID_Kitc.state!="0" && Sonos_Kitc_ZoneGroupID.state!=Sonos_Livi_ZoneGroupID.state && Sonos_Livi_ZoneGroupID.state!=NULL && ((Sonos_Livi_LocalCoordinator.state==ON) || (Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))))){ 
			
			logInfo("openhab","Entered sonos kitchen grouping rule number 3")
			
			if(Sonos_Livi_LocalCoordinator.state==ON){
				Sonos_Livi_Add.sendCommand("RINCON_kitchenplayer")
				logInfo("openhab","Kitchen player added to living room")
			}else if(Sonos_Kitc_LocalCoordinator.state==ON && ((SonosGroupID_Kitc.state=="1" && Sonos_Group1_Number.state=="2") || (SonosGroupID_Kitc.state=="2" && Sonos_Group2_Number.state=="2") || (SonosGroupID_Kitc.state=="3" && Sonos_Group3_Number.state=="2"))) {
				Sonos_Kitc_Add.sendCommand("RINCON_livingroomplayer")
				logInfo("openhab","Living room player added to Kitchen")
			}

		}else{
			Sonos_Kitc_StandAlone.sendCommand("ON")
			logInfo("openhab","Kitchen player set to stand alone")
		}
end

//// UPDATE GROUPING RULES
//
//	The rules will update the appropiate group
//	when a sonos player is grouped, either
//  in the OH2 UI or within the Sonos app.
//	
/////////////////////////////////

rule "Sonos update group membership bathroom"
	when
		Item Sonos_Bath_ZoneGroupID changed
	then
		if(Sonos_Bath_ZoneGroupID.state==Sonos_Kitc_ZoneGroupID.state && Sonos_Kitc_ZoneGroupID.state!=NULL) {
			updateSingleGroups.apply(Sonos_Group1_Number, Sonos_Group2_Number, Sonos_Group3_Number, SonosGroupID_Bath, SonosGroupID_Kitc, "Bathroom")

		} else if(Sonos_Bath_ZoneGroupID.state==Sonos_Gues_ZoneGroupID.state && Sonos_Gues_ZoneGroupID.state!=NULL) {
			updateSingleGroups.apply(Sonos_Group1_Number, Sonos_Group2_Number, Sonos_Group3_Number, SonosGroupID_Bath, SonosGroupID_Gues, "Bathroom")

		} else if(Sonos_Bath_ZoneGroupID.state==Sonos_Livi_ZoneGroupID.state && Sonos_Livi_ZoneGroupID.state!=NULL) {
			updateSingleGroups.apply(Sonos_Group1_Number, Sonos_Group2_Number, Sonos_Group3_Number, SonosGroupID_Bath, SonosGroupID_Livi, "Bathroom")

		// The zone-id changed, but is not grouped with any other players (removed from group).
		// To allow grouping in the UI without the state being reset to single, a timer is applied.
		// This works since grouping in both the OH UI and the Sonos app usually is not performed simultaneously.
		} else {
			if(CheckSinglePlayerTimer===null) {
				CheckSinglePlayerTimer = createTimer(now.plusSeconds(20), [|
					checkSinglePlayers.apply(Sonos_Bath_ZoneGroupID, Sonos_Kitc_ZoneGroupID, Sonos_Gues_ZoneGroupID, Sonos_Livi_ZoneGroupID, SonosGroupID_Bath,"Bathroom")
					checkSinglePlayers.apply(Sonos_Kitc_ZoneGroupID, Sonos_Bath_ZoneGroupID, Sonos_Gues_ZoneGroupID, Sonos_Livi_ZoneGroupID, SonosGroupID_Kitc,"Kitchen")
					checkSinglePlayers.apply(Sonos_Gues_ZoneGroupID, Sonos_Bath_ZoneGroupID, Sonos_Kitc_ZoneGroupID, Sonos_Livi_ZoneGroupID, SonosGroupID_Gues,"Guest room")
					checkSinglePlayers.apply(Sonos_Livi_ZoneGroupID, Sonos_Bath_ZoneGroupID, Sonos_Kitc_ZoneGroupID, Sonos_Gues_ZoneGroupID, SonosGroupID_Livi,"Living room")
					CheckSinglePlayerTimer = null
				])
				logInfo("openhab","CheckSinglePlayerTimer scheduled")
			} else {
				CheckSinglePlayerTimer.reschedule(now.plusSeconds(20))
				logInfo("openhab","CheckSinglePlayerTimer rescheduled")
			}
		} 

end

///// GROUP VISIBILITY
//
//	The rule is used to control the UI.
//  Specifically, it used to determin
//  if a group should be hidden and if
//  no, which player is to be shown within
//  the group. This has to be the local
//	coordinator in the group.
// 
/////////////////////////////////

rule "Sonos group visibility"
	when
		Item SonosGroupID_Kitc received update or
		Item SonosGroupID_Gues received update or
		Item SonosGroupID_Bath received update or
		Item SonosGroupID_Livi received update 
	then
		if(SonosGroupVisibility===null) {
			SonosGroupVisibility = createTimer(now.plusSeconds(5), [|

				if(Sonos_Group1_Number.state<=1){
					postUpdate(Sonos_Group1_Visibility,"Hide")

				}else if(Sonos_Group1_Number.state>=2){
					if(SonosGroupID_Kitc.state=="1" && Sonos_Kitc_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group1_Visibility,"Kitchen")
					}else if(SonosGroupID_Gues.state=="1" && Sonos_Gues_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group1_Visibility,"Guest room")
					}else if(SonosGroupID_Bath.state=="1" && Sonos_Bath_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group1_Visibility,"Bathroom")
					}else if(SonosGroupID_Livi.state=="1" && Sonos_Livi_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group1_Visibility,"Living room")
					}
				}

				if(Sonos_Group2_Number.state<=1){
					postUpdate(Sonos_Group2_Visibility,"Hide")
				}else if(Sonos_Group2_Number.state>=2){
					if(SonosGroupID_Kitc.state=="2" && Sonos_Kitc_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group2_Visibility,"Kitchen")
					}else if(SonosGroupID_Gues.state=="2" && Sonos_Gues_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group2_Visibility,"Guest room")
					}else if(SonosGroupID_Bath.state=="2" && Sonos_Bath_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group2_Visibility,"Bathroom")
					}else if(SonosGroupID_Livi.state=="2" && Sonos_Livi_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group2_Visibility,"Living room")
					}
				}

				if(Sonos_Group3_Number.state<=1){
					postUpdate(Sonos_Group3_Visibility,"Hide")
				}else if(Sonos_Group3_Number.state>=2){
					if(SonosGroupID_Kitc.state=="3" && Sonos_Kitc_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group3_Visibility,"Kitchen")
					}else if(SonosGroupID_Gues.state=="3" && Sonos_Gues_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group3_Visibility,"Guest room")
					}else if(SonosGroupID_Bath.state=="3" && Sonos_Bath_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group3_Visibility,"Bathroom")
					}else if(SonosGroupID_Livi.state=="3" && Sonos_Livi_LocalCoordinator.state==ON){
						postUpdate(Sonos_Group3_Visibility,"Living room")
					}
				}

			])
			logInfo("openhab","SonosGroupVisibility scheduled")
			SonosGroupVisibility = null

		} else {
			SonosGroupVisibility.reschedule(now.plusSeconds(5))
			logInfo("openhab","SonosGroupVisibility rescheduled")
		}

end

I think there is more than one restriction :frowning:
More than 4 Sonos boxes will probably not work, because you can give the checkSinglePlayers function only 3 ZonegroupID.

Are my thoughts right or do I overlook something?

A WorkARound could be to run the checkSinglePlayers function 2 times for each CheckZonegroupID. The function must then be adapted to the number of boxes.

@torsteinsunde I tried again today and failed again. No grouping possible.
Could you please share your whole items and rules files for me too?

@anfaenger if you are running OH3 you could try the following sonos grouping.

https://community.openhab.org/t/sonos-player-widget-for-oh3-mainui/108327/3