Sonos Multiroom rule for openHAB 3

Today I want to share my new Sonos multiroom rule that is made in the new openHAB 3 script language with great javascript support.

Maybe some of you know maybe my old version of openHAB sonos multiroom but this was really ugly code. To the time where i wrote this, there was no javascript support available so I had to use the ugly lambda functions. Now on openHAB 3.0 i decided to make a new fresh version of the rule which uses the benefits of javascript.

What does the rule:

The rule can manage all your sonos speakers and define/modify speaker groups. The whole thing is controlled by a single item. There is no more need to strugle with sonos player groups. Just send the command to the rule and a player will be startet, added to another player or removed from another player.

  • Multi zone support -> The rule can handle multiple zones at the same time

  • Play -> This plays a default defined uri on the given player or on all players.

  • Add -> This adds a player to another player

  • Remove -> This removes a player from the group. If the player is the zone coordinator. This role will be switched to another player befote the player will be removed. So the other speakers continue with the music.

  • Standalone -> This will remove all other players that are part of the zone. The coordinator role will be set to this player

  • Auto ungrouping -> This feature can be enabled by adding the optional second rule of this post. It will ungroup inactive players after a user defined time.

  • Volume control -> If you use my sonos widget for the main ui which is linked below, you can use a volume control similar to the one in the sonos app. You can change the volume single speakers Therefore or for all players in the zone. To use this you need to install the volume control rule which is described below.

How can the rule be installed:

1. Add the following items for each sonos speaker.

  • conrol (Player)
  • volume (Dimmer)

  • add (String)

  • remove (string)

  • standalone (Switch)

  • coordinator (String)

  • (only one) tuneinstationid / playuri (String)

2. Add all sonos items to a group

  • Create a new group named “Group_Sonos” and add all Sonos items in this group. The items can also be nested members in this group. if you like this structure more.

3. Create a control item for the rule

In my case I created a item with the name “Sonos_Multiroom_Controll” as string. but you can use any name that you want.

4. Create the new rule

  • Create a new rule in the mainUI “Rules” section.

  • Open the “Code” tab, remove all parts of it and paste the below code inside it.

  • Go back to “Design” tab. Select the item from step 3 as trigger item of this rule and save.

triggers:
  - id: "1"
    configuration:
      ? itemName
    type: core.ItemCommandTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: "//speaker array needs to be filled for each player -> \"zone name\",
        \"device name\", \"device UUID\"

        var sonosSpeakers = [

        \  [\"Living room\", \"Sonos_Play5_Livingroom\",
        \"RINCON_XXXXXXXXXXXXXXXX\"],

        \  [\"Bed room\", \"Sonos_Play1_Bedroom\",
        \"RINCON_XXXXXXXXXXXXXXXX\"],

        \  [\"Bath room\", \"Sonos_Play1_Bathroom\",
        \"RINCON_XXXXXXXXXXXXXXXX\"]

        ];


        //default volume in hours

        var defaultSpeakerVolume = [8, 8, 8, 8, 8, 8, 8, 10, 12, 14, 16,
        18, 18, 20, 20, 20, 20, 18, 18, 16, 14, 12, 10, 8]


        //default play uri can pe a uri or tunein station id depending
        of the playuri item

        var defaultPlayUri = \"24878\"


        //group where all sonos items belong to (can be a nested group)

        var itemGroupName = \"Group_Sonos\";


        //Player item extension (defines the player status)

        var itemPlayExtensionName = \"_Control\";


        //Volume item extension (controls the volume of the player)

        var itemVolumeExtensionName = \"_Volume\";


        //Add-speaker item extension (adds a speaker to the zone
        coordinator)

        var itemAddExtensionName = \"_Add\";


        //Remove-speaker item extension (removes speaker from the zone
        coordinator)

        var itemRemoveExtensionName = \"_Remove\";


        //Play-Uri item extension (needed to play a uri on a new group)

        var itemPlayUriExtensionName = \"_Tuneinstationid\";


        //Standalone item extension (needed to remove player if this is
        the coordinator in a multiroom zone)

        var itemStandaloneExtensionName = \"_Standalone\";


        //Coordinator item extension (displays the master of this zone)

        var itemMasterExtensionName = \"_Coordinator\";



        /*
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Global
        functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /*
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */


        //get speaker volume

        function getSpeakerVolume(mySpeaker){

        \  return itemRegistry.getItem(mySpeaker +
        itemVolumeExtensionName).getState();

        }


        //set speaker volume

        function setSpeakerVolume(mySpeaker, volume){

        \  if(getSpeakerVolume(mySpeaker).toString() !=
        volume.toString()){

        \    events.sendCommand(mySpeaker + itemVolumeExtensionName,
        volume);

        \  }

        }


        //get coordinator of zone

        function getCoordinator(mySpeaker){

        \  return itemRegistry.getItem(mySpeaker +
        itemMasterExtensionName).getState();

        }


        //add speaker to active speaker

        function addSpeaker(mySpeaker, myNewSpeakerUUID){

        \  //get coordinator of active speaker

        \  var coordinator =
        getSpeakerName(getCoordinator(mySpeaker).toString())

        \  //add new player to the coordinator

        \  events.sendCommand(coordinator + itemAddExtensionName,
        myNewSpeakerUUID);

        }


        //play uri on speaker

        function playURI(mySpeaker){

        \  events.sendCommand(mySpeaker + itemPlayUriExtensionName,
        defaultPlayUri);

        }


        //remove speaker from group

        function removeSpeaker(mySpeaker){

        \  //get coordinator of this group

        \  var coordinator =
        getSpeakerName(getCoordinator(mySpeaker).toString())

        \  //check if speaker is current coordinator

        \  if(mySpeaker == coordinator){

        \    //remove speaker form the coordinator

        \    events.sendCommand(coordinator +
        itemStandaloneExtensionName, \"ON\");

        \  }

        \  else{

        \    events.sendCommand(coordinator + itemRemoveExtensionName,
        getSpeakerUUID(mySpeaker));

        \  }

        }


        //remove all speakers expect the given one from group

        function removeAllOtherSpeakers(mySpeakerName){

        \  //Get Groupid where my speaker belongs to

        \  var mySpeaker = itemRegistry.getItem(mySpeakerName +
        itemMasterExtensionName);

        \  //loop in list to get all items with the same group id

        \  for(var i in sonosMembers){

        \    if(sonosMembers[i].getState().toString() ==
        mySpeaker.getState().toString() &&
        sonosMembers[i].getName().toString().endsWith(itemMasterExtensionName)
        ){

        \      //check if current item is speaker which should stay in
        this group

        \      if(sonosMembers[i] != mySpeaker){

        \        logger.info(\"Speaker to remove \" +
        sonosMembers[i].getName())

        \        //remove speaker

        \        events.sendCommand(getSpeakerNameFromItem(sonosMembers\
        [i].getName()) + itemStandaloneExtensionName, \"ON\")

        \        //removeSpeaker(getSpeakerNameFromItem(sonosMembers[i]\
        .getName()))

        \      }

        \    }

        \  }

        }


        //returns all active player

        function getActiveSpeakers(){

        \  var result = [];

        \  //loop in list to get all items with state \"PLAY\"

        \  for(var i in sonosMembers){

        \    if(sonosMembers[i].getState().toString() == \"PLAY\"){

        \      result.push(sonosMembers[i]);

        \    }

        \  }

        \  return result;

        }


        //checks if a given speaker if active

        function isSpeakerActive(mySpeaker){

        \  if(itemRegistry.getItem(mySpeaker +
        itemPlayExtensionName).getState().toString() == \"PLAY\"){

        \    return true;

        \  }

        }


        // Get speaker name from item name

        function getSpeakerNameFromItem(myItem){

        \    //Get last substring of string, split by \"_\"

        \tvar lastItem = myItem.substring(myItem.lastIndexOf('_') + 1);

        \    //Remove substring and \"_\" from the given string and
        return this

        \    return myItem.substring(0, myItem.length() -
        lastItem.length() - 1);

        }


        // get Speaker zonename

        function getSpeakerZoneName(mySpeaker) {

        \  //loop in speakers array

        \  for (var i in sonosSpeakers) {

        \    //get subarray

        \    var speaker = sonosSpeakers[i];

        \    //check if speaker is found in this subarray

        \    if (speaker[0] == mySpeaker || speaker[1] == mySpeaker ||
        speaker[2] == mySpeaker) {

        \      return speaker[0]

        \    }

        \  }

        \  return null

        }


        // get Speaker namne

        function getSpeakerName(mySpeaker) {

        \  //loop in speakers array

        \  for (var i in sonosSpeakers) {

        \    //get subarray

        \    var speaker = sonosSpeakers[i];

        \    //check if speaker is found in this subarray

        \    if (speaker[0] == mySpeaker || speaker[1] == mySpeaker ||
        speaker[2] == mySpeaker) {

        \      return speaker[1]

        \    }

        \  }

        \  return null

        }


        // get Speaker UUID

        function getSpeakerUUID(mySpeaker) {

        \  //loop in speakers array

        \  for (var i in sonosSpeakers) {

        \    //get subarray

        \    var speaker = sonosSpeakers[i];

        \    //check if speaker is found in this subarray

        \    if (speaker[0] == mySpeaker || speaker[1] == mySpeaker ||
        speaker[2] == mySpeaker) {

        \      return speaker[2]

        \    }

        \  }

        \  return null

        }


        // returns the default volume for this hour

        function getDefaultSpeakerVolume(){

        \  var now = new Date().getHours();

        \  return defaultSpeakerVolume[now]

        }


        //function to handle play command

        function playAnywhere(){

        \  //get first active speaker

        \  var activeSpeakerPlayer = getActiveSpeakers()[0];

        \ \ 

        \  //check if active speaker was found

        \  if(activeSpeakerPlayer !== undefined){

        \    //add player to active player zone

        \    addPlayerToActiveZone(activeSpeakerPlayer);

        \  }

        \  else{

        \    //create new player zone

        \    createPlayerZone()

        \  }

        }


        //add player to active zone

        function addPlayerToActiveZone(activeSpeakerPlayer){

        \  //get active speaker name

        \  var activeSpeaker =
        getSpeakerNameFromItem(activeSpeakerPlayer.getName());


        \  logger.info(\"Adding \" +
        getSpeakerZoneName(speakerToControl) + \" to the existing multiroom zone
        \" + getSpeakerZoneName(activeSpeaker) + \".\");


        \  //set new speaker volume to active speaker volume

        \  setSpeakerVolume(speakerToControl,
        getSpeakerVolume(activeSpeaker));


        \  //add speaker to active speaker

        \  addSpeaker(activeSpeaker, getSpeakerUUID(speakerToControl));

        }


        //start new zone for this player

        function createPlayerZone(){

        \  logger.info(\"Creating new multiroom zone for \" +
        getSpeakerZoneName(speakerToControl) + \".\");

        \   \ 

        \  //seet default volume

        \  setSpeakerVolume(speakerToControl,
        getDefaultSpeakerVolume());


        \  //play default uri on speaker

        \  playURI(speakerToControl);

        }


        //set given player to standalone player

        function setPlayerStandalone(){

        \  //if player is not part of any zone ad him to a zone

        \  if(!(isSpeakerActive(speakerToControl))){

        \    //get all active players

        \    var activeSpeakerPlayer = getActiveSpeakers()[0];

        \    //check if an active player was found

        \    if(activeSpeakerPlayer !== undefined){

        \      //add player to active zone

        \      addPlayerToActiveZone(activeSpeakerPlayer);

        \      //wait until player is member of this zone

        \      while(getCoordinator(speakerToControl).toString() !=
        getCoordinator(getSpeakerNameFromItem(activeSpeakerPlayer.getName())).t\
        oString()){

        \        java.lang.Thread.sleep(1000);

        \      }

        \    }

        \    else{

        \      //create new zone for player because no player active at
        the moment

        \      createPlayerZone();

        \    }

        \  }

        \ \ 

        \  //remove all other player

        \  logger.info(\"Removing all player from the multiroom zone \"
        + getSpeakerZoneName(speakerToControl) + \".\");

        \  removeAllOtherSpeakers(speakerToControl);

        }


        /*
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ rule
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /*
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */


        //create logger

        var logger =
        Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' +
        \"Sonos\");

        logger.info(\"Multiroom rule started.\")


        //get all members of the sonos group

        var sonosMembers =
        Java.from(itemRegistry.getItem(itemGroupName).getAllMembers());


        //read trigger command if exists

        if(command !== undefined){

        \  //read input variable

        \  var sonosCommand = command.toString().split(':')[0];

        \  var speakerToControl =
        getSpeakerName(command.toString().split(':')[1]);


        \  //enter this if command is \"add\"

        \  if(sonosCommand == \"Add\"){

        \    //get destination and control speaker speaker

        \    var destinationPlayer =
        getSpeakerName(command.toString().split(':')[1].split('@')[1]);

        \    speakerToControl =
        getSpeakerName(command.toString().split(':')[1].split('@')[0]);

        \    //get coordinator of destination player

        \    var coordinator = getCoordinator(destinationPlayer);

        \    //check if new coordinator is playing

        \    if(isSpeakerActive(getSpeakerName(coordinator))){

        \      //add speaker to this coordinator

        \      addSpeaker(getSpeakerName(coordinator),
        getSpeakerUUID(speakerToControl));

        \    }

        \    else{

        \      //if current spe3aker active add the destination to
        current speaker

        \      if(isSpeakerActive(speakerToControl)){

        \        //add speaker to this coordinator

        \        addSpeaker(speakerToControl,
        getSpeakerUUID(coordinator));

        \      }

        \      else{

        \        //add speaker to coordinator because no other active
        speaker in selection found

        \      addSpeaker(getSpeakerName(coordinator),
        getSpeakerUUID(coordinator));

        \      }

        \    }

        \  }

        \ \ 

        \  //enter this if command is \"Play\" and the given speaker is
        currently inactive

        \  if(sonosCommand == \"Play\"){

        \    //get destination and control speaker speaker

        \    var destinationPlayer =
        command.toString().split(':')[1].split('@')[1];

        \    speakerToControl =
        command.toString().split(':')[1].split('@')[0];

        \   \ 

        \    //Create new zone and play default uri

        \    if(destinationPlayer == speakerToControl ||
        destinationPlayer == \"new\") {

        \      speakerToControl = getSpeakerName(speakerToControl);

        \      //Play music in new zone

        \      createPlayerZone()

        \    }

        \   \ 

        \    //Search for any zone if no found start new

        \    if(destinationPlayer == \"all\") {

        \      speakerToControl = getSpeakerName(speakerToControl);

        \      //Play music in new zone

        \      createPlayerZone()

        \      //add all other players

        \      //loop in speakers array

        \      for (var i in sonosSpeakers) {

        \        //get subarray

        \        var speaker = sonosSpeakers[i];

        \        //check if speaker is found in this subarray

        \        if (speaker[1] != speakerToControl) {

        \          addSpeaker(speakerToControl, speaker[2]);

        \        }

        \      }

        \    }

        \   \ 

        \    //Search for any zone if no found start new

        \    if(destinationPlayer == \"*\") {

        \      speakerToControl = getSpeakerName(speakerToControl);

        \      //Play music in new zone

        \      playAnywhere()

        \    }

        \  }


        \  //enter this if command is \"Remove\"

        \  if(sonosCommand == \"Remove\"){

        \    logger.info(\"Removing \" +
        getSpeakerZoneName(speakerToControl) + \" from the related multiroom
        zone.\");

        \    removeSpeaker(speakerToControl);

        \  }


        \  //enter this if command is \"Standalone\"

        \  if(sonosCommand == \"Standalone\"){

        \    //set the given player as standalone player

        \    setPlayerStandalone();

        \  }

        }

        //If rule was not triggered by an command enter this branche

        else{

        \  //get item that triggered the command

        \  var triggerItemName = event.itemName.toString();

        \  logger.info(triggerItemName);

        }

        \ \ 

        \  "
    type: script.ScriptAction

  

5. Modify the rule

Open the script of the rule again and fill the fields of the section User input required. There are already example values inside. Maybe some of them ar efine but please check.

For each player you must provide the zone details like:

  • Item friendly name like “living room”

  • Item main name (“Sonos_Play5_Livingroom_Control” without e.g. “_Control”)

  • The speaker UUID

6. Send commands and control the zoneplayer

You can now send commands to the rule to control the players.

  • Command: “Play:Living room@Living room” -> Will start a new zone in living room and play the default uri in the default volume of the hour (if configured in the rule).

  • Command: “Play:Living room@all” -> Will start a new zone with all players and play the default uri in the default volume of the hour (if configured in the rule).

  • Command: “Play:Living room@*” -> The rule searchs for any active player. If one is found the player will be added, if no active player found a new zone will startet in living room with the current volume

  • Command: “Add:Living room@Bed room” -> The rule will add Living room to bedroom.

  • Command: “Remove:Living room” -> The rule will remove Living roomfrom each zone and stops music on thsi player

  • Command: “Standalone:Living room” -> The rule will remove all other players from this zone so that only living room will stay in the zone.

Additional second rule for automatic ungrouping of inactive players

If you like you can also have an automatic ungrouping of all inactive players after a defined time. This can be done by creating a second rule.

1. Create a new group with all player items

  • Create a group “Group_Sonos_Player”. The group can also have another name if you like

  • Add all sonos Player items as direct members of this group

2. Create a new rule and copy the following inside it.

  • Create a new rule, define a id and switch to “Code” tab.

  • Paste the following code block inside the code tab.

  • Go back to “Desing” tab and selecht the group from step one as trigger for thsi rule an´d save the rule.

triggers:
  - id: "1"
    configuration:
      groupName: 
      state: PAUSE
      previousState: PLAY
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >
        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ User input required! ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */


        //Sonos control item that triggers the main sonos multiroom rule

        var itemSonosControlName = "Sonos_Multiroom_Control";


        //inactivity time of a player before it will be removed from the zone. (default is one minute)

        var inactivityTimeInMinutes = 1;


        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Global functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */


        //checks if a given speaker if active

        function isSpeakerActive(mySpeaker){
          if(itemRegistry.getItem(mySpeaker).getState().toString() == "PLAY"){
            return true;
          }
        }


        // Get speaker name from item name

        function getSpeakerNameFromItem(myItem){
            //Get last substring of string, split by "_"
        	var lastItem = myItem.substring(myItem.lastIndexOf('_') + 1);
            //Remove substring and "_" from the given string and return this
            return myItem.substring(0, myItem.length() - lastItem.length() - 1);
        }


        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ rule ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */


        //create logger

        var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + "SonosMultiroomPlayerUpdateService");

        logger.info("Multiroom player update service started.")


        //get item name from triggering item

        var speakerToControlPlayer = event.itemName



        // Create timer to check if speaker is in one minute still inactive. If yes remove from group

        var Timer    = Java.type("java.util.Timer");

        var timer2 = new Timer();

        var runme2 = function() {
                      //check if speaker is still inactive
                      if(!(isSpeakerActive(speakerToControlPlayer))){
                        logger.info("Removing " + getSpeakerNameFromItem(speakerToControlPlayer) + " from the zone due to " + inactivityTimeInMinutes + " minute inactivity.");
                        events.sendCommand(itemSonosControlName, "Remove:" + getSpeakerNameFromItem(speakerToControlPlayer));
                      }
                     }
        timer2.schedule( runme2, inactivityTimeInMinutes * 60 * 1000);
    type: script.ScriptAction

3. Modify the rule

Open the script of the rule again and fill the fields of the section User input required.
In this case you only need to provide the following two things:

  • the name of the sonos control item that was created for the main rule before (e.g. “Sonos_Multiroom_Control”).

  • the inactivity time in minutes that is allowed for each player.

4. Send commands

The rule is running in the backround. No need to send some commands here

Volume control rule for my sonos widget

If you use my sonos widget (linked below) you can use this rule to manage the volume of all speakers perfekt. It allows you to control the volume of each single zone member or the complete zone as you know it from the sonos app.

1. Create a group for custom items

  • Create a group e.g. “Group_Sonos_ZoneVolume”. The group can also have another name if you like.

  • Create for each speaker another item e.g. “Sonos_LivingRoom_ZoneVolume”. The item must have the same prefix as the volume or player item.

  • Add all these new _ZoneVolume items to the group you created in the previous step.

  • Create a second group e.g. “Group_Sonos_Volumes” where we add all sonos volume items that are linked to the channel of a thing.

  • Create a third group e.g. “Group_Sonos_Coordinators” and add all coordinator items as member of this group.

2. Create a new rule and copy the following inside it.

  • Create a new rule, define a id and switch to “Code” tab.

  • Paste the following code block inside the code tab.

triggers:
  - id: "3"
    configuration:
      groupName: 
    type: core.GroupCommandTrigger
  - id: "4"
    configuration:
      groupName: 
    type: core.GroupStateChangeTrigger
  - id: "1"
    configuration:
      groupName: 
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >-
        
        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ User inputrequired! ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */


        //group where all sonos items belong to (can be a nested group)

        var itemGroupName = "Group_Sonos";


        //Volume item extension (controls the volume of the player)

        var itemVolumeExtensionName = "_Volume";


        //Zone volume item for the widget. Not connected to any thing item.

        var itemZoneVolumeExtensionName = "_ZoneVolume";


        //Coordinator item extension (displays the master of this zone)

        var itemMasterExtensionName = "_Coordinator";



        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Global functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */


        //get speaker volume

        function getItemState(myItem){
          return itemRegistry.getItem(myItem).getState();
        }


        //get speaker volume

        function getSpeakerVolume(mySpeaker){
          return itemRegistry.getItem(mySpeaker + itemVolumeExtensionName).getState();
        }


        //set speaker volume

        function setSpeakerVolume(mySpeaker, volume){
          if(getSpeakerVolume(mySpeaker).toString() != volume.toString()){
            events.sendCommand(mySpeaker + itemVolumeExtensionName, Math.round(volume));
          }
        }


        //get coordinator of zone

        function getCoordinator(mySpeaker){
          return itemRegistry.getItem(mySpeaker + itemMasterExtensionName).getState();
        }


        //get all members of this coordinator

        function getAllZoneMembers(mySpeaker){
          //get coordinator
          var coordinator = getCoordinator(mySpeaker);
          var result = [];
          //loop in list to get all items with the same coordinator
          for(var i in sonosMembers){
            if(sonosMembers[i].getState().toString() == coordinator.toString()  && sonosMembers[i].getName().toString().endsWith(itemMasterExtensionName)){
              result.push(sonosMembers[i]);
            }
          }
          return result;
        }


        //Calculate the volume of all speakers in the zone

        function getAVGZoneVolume(){
          //calculate current volume to see how much to reduce/increase
          var currentSpeakerVolume = 0;
          for(var i in allZoneMembers){
            //get speaker name
            var currentSpeakerName = getSpeakerNameFromItem(allZoneMembers[i].getName());
            //add speaker volume to volume var
            currentSpeakerVolume = currentSpeakerVolume + getSpeakerVolume(currentSpeakerName);
            //return average value
          }
          return currentSpeakerVolume / allZoneMembers.length;

        }


        // Get speaker name from item name

        function getSpeakerNameFromItem(myItem){
            //Get last substring of string, split by "_"
        	var lastItem = myItem.substring(myItem.lastIndexOf('_') + 1);
            //Remove substring and "_" from the given string and return this
            return myItem.substring(0, myItem.length() - lastItem.length() - 1);
        }


        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ rule ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

        /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */


        //create logger

        var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + "SonosMultiroomVolumeUpdateService");

        logger.debug("Multiroom Volume update service started.")



        //get all members of the sonos group

        var sonosMembers = Java.from(itemRegistry.getItem(itemGroupName).getAllMembers());


        //Get details of the item that triggered the rule

        var triggeringSpeaker = getSpeakerNameFromItem(event.itemName);

        var triggeringChannel = event.itemName;

        var zoneVolume = getItemState(triggeringChannel);


        //get all zonemembers that belong to the zone

        var allZoneMembers = getAllZoneMembers(triggeringSpeaker);


        //if this is an update send directly to a sonos speaker volume, update item _zoneVolume accordingly.

        if(triggeringChannel.replace(triggeringSpeaker, "") == itemVolumeExtensionName){
          //get current volume of all players in the zone
          var currentSpeakerVolume = getAVGZoneVolume();
          
          //send volume to all related zoneVolume widget items 
          //set volume for all these zone members
          for(var i in allZoneMembers){
            //get speaker name
            var currentSpeakerName = getSpeakerNameFromItem(allZoneMembers[i].getName());
            //update the new zone volume item
            events.postUpdate(currentSpeakerName + itemZoneVolumeExtensionName, Math.round(currentSpeakerVolume));
          }
        }



        //if this is an update send by the widget (the item _zoneVolume sends the command and not _volume)

        if(triggeringChannel.replace(triggeringSpeaker, "") == itemZoneVolumeExtensionName){
          
          //calculate volume difference to se how much to reduce/increase
          var currentSpeakerVolume = getAVGZoneVolume();
          logger.info("all volume" + getAVGZoneVolume());
          //calculate difference value
          var diff = zoneVolume - currentSpeakerVolume;

          //set volume for all these zone members
          for(var i in allZoneMembers){
            //get speaker name
            var currentSpeakerName = getSpeakerNameFromItem(allZoneMembers[i].getName());
            //get speaker volume
            currentSpeakerVolume = getSpeakerVolume(currentSpeakerName);
            //set the new volume
            setSpeakerVolume(currentSpeakerName, currentSpeakerVolume + diff);
          }
        }
    type: script.ScriptAction

3. Modify the rule

Open the script of the rule again and fill the fields of the section User input required.
In this case you only need to provide the following things:

  • the name of the sonos group that was created for all sonos items (See main rule). In my example it was “Group_Sonos”.

  • the _volume item
    the _zonevolume item we created in step 1

  • the coordinator item

4. Rule trigger

As rule trigger you need to provide the following two:

  • The group with all _ZoneVolume items -> “When a member of the group receives a command”

  • The group with all _Volume items linkted to a channel -> “When a member of the group changes”

  • The group with all _Coordinator items linkted to a channel -> “When a member of the group changes”

5. How to use
You can use the rule by sending commands to the _ZoneVolume items. I recommend you to use my widget. If you use my widget you just need to open a page where the widget is available. Open the properties of the widget and change the volume item to the _ZoneVolume item. The volume item is still in the player array so the widget knows both :wink:

UI can be found in the following topic:

IMG_1577

5 Likes

@buschif4 KUDOS

Looks great. Will try that definitely!

Maybe someone is able to create some widegt for the Main UI?

@Dibbler42 thanks.

By the way there is an widget for this available. I posted this here :wink:

Sonos Player widget for OH3 mainUI

1 Like

Saw it just 10 posts later :slight_smile:

Hello Flo,

first of all, thanks for this awesome rule and the widget that goes along with it.
I haven’t used the old version before, so for me this was the first time setting it up.
Here are some notes where I got stuck during the setup.

  • The items in step 1 must all be linked to the corresponding channel. I guess this was one was obvious, but for some reason the thing did not show all channels in the UI list (edit: did forget to set the advanced checkbox). But since I created the speaker using files, it was fixed after a quick look at the binding documentation.
  • For the volume control rule, you could add in step 1 that you also need a group with all volume items since this is needed as trigger in step 4.

I also have some notes on the widget which I will add to the widget post.

So once again thanks for sharing your work!

Hello Felix,

I updated the documentation regarding the group.

The items in step one must be created for each speaker so it should be clear that they mus be linked to a channel. :wink:

1 Like

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.