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