Monoprice 6-zone Audio amp items, sitemap & rules

I figured I’d start this topic, just in case anyone else is using the Monoprice 6-zone home audio amp. I’ve been working on incorporating it into my OH setup to be able to control the zones via OH, but it’s still a work in progress. I am able to control the amp well (volume, source, etc), but still have to work on the rules that show status from the amp (I think I’m running into issues with timing the response parsing, and actually regex’ing the input). I’m kind of hoping that the community helps me develop this into a more suitable set of items/sitemap/rules for anyone out there that has this amp and would like to incorporate it into their OH system.

BTW, I’m using an RPI2 with a USB-RS232 adapter…

EDIT 28 Sept 2015: I found the time to fix my issues, and now have a reliably working set of rules/items/sitemaps configs to actually make this usable. Power/Source/Volume/Mute works, and status parsing appears reliable. All statuses are parsed out, but not everything is used. This can easily be extended to control Bass/Treble/Balance and the other options that are available. Let me know if you use it or have issues with it!

EDIT 1 March 2018: Incorporating @swamiller 's excellent work on simplifying and extending this code to work for multiple zones and all control functions, using much simpler design patterns (thanks to @rlkoshak 's suggestions, as well!). If you’re using a direct serial interface, use the items/rules/sitemap entries posted below. If you’re using a GlobalCache iTach Flex, use the code in post 5 from @swamiller.

EDIT 23 May 2020: @mlobstein wrote and published a brand new, COMPLETE binding for the Monoprice 10761 amp (and the other amps that follow the same serial API), see his post below to download the JAR. See my post below that for the updated .items and .sitemap config utilizing the new binding (all of the features are available, and the amp response is much faster than when using the old rules). I will leave the original items/sitemap/rules below for now, until the binding is formally merged with an upcoming release of OH (also for nostalgia, these rules served me well for the past 5+ years :smiley: ). Enjoy!

ITEMS:


Group gZonePowerSwitch (all)
Group gZoneVolume (all)
Group gZoneMuteSwitch (all)
Group gZoneTone (all)
Group gZoneBalance (all)
Group gZoneSource (all)
Group gMonoPriceStatus (all)

String AudioMTX             "Audio Matrix"           { serial="/dev/ttyUSB0@9600" }
//String AudioMTX_Receive     "Audio Matrix Receive"
Switch MonoPrice_Refresh    "Refresh String"
//String MonoPrice_Receive_String "Received Response"
String MonoPrice_Status     "MonoPrice Status [%s]"
String MonoPrice_Status_G1  "MP G1 (Zone) [%s]"             (gMonoPriceStatus)
String MonoPrice_Status_G2  "MP G2 (PA Status) [%s]"        (gMonoPriceStatus)
String MonoPrice_Status_G3  "MP G3 (Pwr Status) [%s]"       (gMonoPriceStatus)
String MonoPrice_Status_G4  "MP G4 (Mute Status) [%s]"      (gMonoPriceStatus)
String MonoPrice_Status_G5  "MP G5 (DT Status) [%s]"        (gMonoPriceStatus)
Number MonoPrice_Status_G6  "MP G6 (Volume Status) [%.0f]"  (gMonoPriceStatus)
Number MonoPrice_Status_G7  "MP G7 (Treble Status) [%.0f]"  (gMonoPriceStatus)
Number MonoPrice_Status_G8  "MP G8 (Bass Status) [%.0f]"    (gMonoPriceStatus)
Number MonoPrice_Status_G9  "MP G9 (Balance) [%.0f]"        (gMonoPriceStatus)
Number MonoPrice_Status_G10 "MP G10 (Src Status) [%.0f]"    (gMonoPriceStatus)
String MonoPrice_Status_G11 "MP G11 (Keypad Status) [%s]"   (gMonoPriceStatus)
String MonoPrice_Z11         "Zone 1 Status [%s]"           
String MonoPrice_Z12         "Zone 2 Status [%s]"          
String MonoPrice_Z13         "Zone 3 Status [%s]"           
String MonoPrice_Z14         "Zone 4 Status [%s]"          
String MonoPrice_Z15         "Zone 5 Status [%s]"          
String MonoPrice_Z16         "Zone 6 Status [%s]"           
Switch MonoPrice_Z11_Power   "Zone 1 Power [%s]"        <switch>		 (gZonePowerSwitch) [ "Switchable" ]
Switch MonoPrice_Z12_Power   "Zone 2 Power [%s]"        <switch>		 (gZonePowerSwitch) [ "Switchable" ]
Switch MonoPrice_Z13_Power   "Zone 3 Power [%s]"        <switch>		 (gZonePowerSwitch) [ "Switchable" ]
Switch MonoPrice_Z14_Power   "Zone 4 Power [%s]"        <switch>		 (gZonePowerSwitch) [ "Switchable" ]
Switch MonoPrice_Z15_Power   "Zone 5 Power [%s]"        <switch>		 (gZonePowerSwitch) [ "Switchable" ]
Switch MonoPrice_Z16_Power   "Zone 6 Power [%s]"        <switch> 	 (gZonePowerSwitch) [ "Switchable" ]
Switch MonoPrice_Z11_Mute    "Zone 1 Mute [%s]"   	   <mute> 		 (gZoneMuteSwitch) [ "Switchable" ]
Switch MonoPrice_Z12_Mute    "Zone 2 Mute [%s]"    	   <mute> 		 (gZoneMuteSwitch) [ "Switchable" ]
Switch MonoPrice_Z13_Mute    "Zone 3 Mute [%s]"    	   <mute> 		 (gZoneMuteSwitch) [ "Switchable" ]
Switch MonoPrice_Z14_Mute    "Zone 4 Mute [%s]"    	   <mute> 		 (gZoneMuteSwitch) [ "Switchable" ]
Switch MonoPrice_Z15_Mute    "Zone 5 Mute [%s]"     	   <mute> 		 (gZoneMuteSwitch) [ "Switchable" ]
Switch MonoPrice_Z16_Mute    "Zone 6 Mute [%s]"    	   <mute> 		 (gZoneMuteSwitch) [ "Switchable" ]
Dimmer MonoPrice_Z11_Volume  "Zone 1 Volume [%02d %%]"  <soundvolume> (gZoneVolume) [ "Switchable" ]
Dimmer MonoPrice_Z12_Volume  "Zone 2 Volume [%02d %%]"  <soundvolume> (gZoneVolume) [ "Switchable" ]
Dimmer MonoPrice_Z13_Volume  "Zone 3 Volume [%02d %%]"  <soundvolume> (gZoneVolume) [ "Switchable" ]
Dimmer MonoPrice_Z14_Volume  "Zone 4 Volume [%02d %%]"  <soundvolume> (gZoneVolume) [ "Switchable" ]
Dimmer MonoPrice_Z15_Volume  "Zone 5 Volume [%02d %%]"  <soundvolume> (gZoneVolume) [ "Switchable" ]
Dimmer MonoPrice_Z16_Volume  "Zone 6 Volume [%02d %%]"  <soundvolume> (gZoneVolume) [ "Switchable" ]
Dimmer MonoPrice_Z11_Treble  "Zone 1 Treble [%02d %%]"  <treble>      (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z12_Treble  "Zone 2 Treble [%02d %%]"  <treble>      (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z13_Treble  "Zone 3 Treble [%02d %%]"  <treble>      (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z14_Treble  "Zone 4 Treble [%02d %%]"  <treble>      (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z15_Treble  "Zone 5 Treble [%02d %%]"  <treble>      (gZoneTone)  [ "Switchable" ]
Dimmer MonoPrice_Z16_Treble  "Zone 6 Treble [%02d %%]"  <treble>	     (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z11_Bass    "Zone 1 Bass [%02d %%]"  	<bass>		 (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z12_Bass    "Zone 2 Bass [%02d %%]"  	<bass>		 (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z13_Bass    "Zone 3 Bass [%02d %%]"   	<bass>		 (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z14_Bass    "Zone 4 Bass [%02d %%]"   	<bass>		 (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z15_Bass    "Zone 5 Bass [%02d %%]"    	<bass>		 (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z16_Bass    "Zone 6 Bass [%02d %%]"    	<bass>		 (gZoneTone) [ "Switchable" ]
Dimmer MonoPrice_Z11_Balance "Zone 1 Balance [%02d %%]" 	<control>	 (gZoneBalance) [ "Switchable" ]
Dimmer MonoPrice_Z12_Balance "Zone 2 Balance [%02d %%]" 	<control>	 (gZoneBalance) [ "Switchable" ]
Dimmer MonoPrice_Z13_Balance "Zone 3 Balance [%02d %%]" 	<control>	 (gZoneBalance) [ "Switchable" ]
Dimmer MonoPrice_Z14_Balance "Zone 4 Balance [%02d %%]" 	<control>	 (gZoneBalance) [ "Switchable" ]
Dimmer MonoPrice_Z15_Balance "Zone 5 Balance [%02d %%]" 	<control>	 (gZoneBalance) [ "Switchable" ]
Dimmer MonoPrice_Z16_Balance "Zone 6 Balance [%02d %%]" 	<control>	 (gZoneBalance) [ "Switchable" ]
Number MonoPrice_Z11_Source  "Zone 1 Source [%.0f]" 	<control>		 (gZoneSource)
Number MonoPrice_Z12_Source  "Zone 2 Source [%.0f]" 	<control>		 (gZoneSource)
Number MonoPrice_Z13_Source  "Zone 3 Source [%.0f]"	<control>		 (gZoneSource)
Number MonoPrice_Z14_Source  "Zone 4 Source [%.0f]"	<control>		 (gZoneSource)
Number MonoPrice_Z15_Source  "Zone 5 Source [%.0f]"	<control>		 (gZoneSource)
Number MonoPrice_Z16_Source  "Zone 6 Source [%.0f]"	<control>		 (gZoneSource)

SITEMAP:

Frame label="MonoPrice" {
		Switch item=MonoPrice_Refresh
		//Text item=AudioMTX_Receive
		//Text item=MonoPrice_Status
		//Text item=MonoPrice_Status_G1
		// Text item=MonoPrice_Status_G2
		// Text item=MonoPrice_Status_G3
		// Text item=MonoPrice_Status_G4
		// Text item=MonoPrice_Status_G5
		// Text item=MonoPrice_Status_G6
		// Text item=MonoPrice_Status_G7
	    // Text item=MonoPrice_Status_G8
		// Text item=MonoPrice_Status_G9
		// Text item=MonoPrice_Status_G10
		// Text item=MonoPrice_Status_G11
		Text item=MonoPrice_Z11
		Switch item=MonoPrice_Z11_Power
		Selection item=MonoPrice_Z11_Source label="Zone 1 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z11_Power==ON]
		Slider item=MonoPrice_Z11_Volume visibility=[MonoPrice_Z11_Power==ON]
		Switch item=MonoPrice_Z11_Mute visibility=[MonoPrice_Z11_Power==ON]
		Slider item=MonoPrice_Z11_Treble visibility=[MonoPrice_Z11_Power==ON]
		Slider item=MonoPrice_Z11_Bass visibility=[MonoPrice_Z11_Power==ON]
		Slider item=MonoPrice_Z11_Balance visibility=[MonoPrice_Z11_Power==ON]
		Text item=MonoPrice_Z12
		Switch item=MonoPrice_Z12_Power
		Selection item=MonoPrice_Z12_Source label="Zone 2 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z12_Power==ON]
		Slider item=MonoPrice_Z12_Volume visibility=[MonoPrice_Z12_Power==ON]
		Switch item=MonoPrice_Z12_Mute visibility=[MonoPrice_Z12_Power==ON]
		Slider item=MonoPrice_Z12_Treble visibility=[MonoPrice_Z12_Power==ON]
		Slider item=MonoPrice_Z12_Bass visibility=[MonoPrice_Z12_Power==ON]
		Slider item=MonoPrice_Z12_Balance visibility=[MonoPrice_Z12_Power==ON]
		Text item=MonoPrice_Z13
		Switch item=MonoPrice_Z13_Power
		Selection item=MonoPrice_Z13_Source label="Zone 3 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z13_Power==ON]
		Slider item=MonoPrice_Z13_Volume visibility=[MonoPrice_Z13_Power==ON]
		Switch item=MonoPrice_Z13_Mute visibility=[MonoPrice_Z13_Power==ON]
		Slider item=MonoPrice_Z13_Treble visibility=[MonoPrice_Z13_Power==ON]
		Slider item=MonoPrice_Z13_Bass visibility=[MonoPrice_Z13_Power==ON]
		Slider item=MonoPrice_Z13_Balance visibility=[MonoPrice_Z13_Power==ON]
		Text item=MonoPrice_Z14
		Switch item=MonoPrice_Z14_Power
		Selection item=MonoPrice_Z14_Source label="Zone 4 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z14_Power==ON]
		Slider item=MonoPrice_Z14_Volume visibility=[MonoPrice_Z14_Power==ON]
		Switch item=MonoPrice_Z14_Mute visibility=[MonoPrice_Z14_Power==ON]
		Slider item=MonoPrice_Z14_Treble visibility=[MonoPrice_Z14_Power==ON]
		Slider item=MonoPrice_Z14_Bass visibility=[MonoPrice_Z14_Power==ON]
		Slider item=MonoPrice_Z14_Balance visibility=[MonoPrice_Z14_Power==ON]
		Text item=MonoPrice_Z15
		Switch item=MonoPrice_Z15_Power
		Selection item=MonoPrice_Z15_Source label="Zone 5 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z15_Power==ON]
		Slider item=MonoPrice_Z15_Volume visibility=[MonoPrice_Z15_Power==ON]
		Switch item=MonoPrice_Z15_Mute visibility=[MonoPrice_Z15_Power==ON]
		Slider item=MonoPrice_Z15_Treble visibility=[MonoPrice_Z15_Power==ON]
		Slider item=MonoPrice_Z15_Bass visibility=[MonoPrice_Z15_Power==ON]
		Slider item=MonoPrice_Z15_Balance visibility=[MonoPrice_Z15_Power==ON]
		Text item=MonoPrice_Z16
		Switch item=MonoPrice_Z16_Power
		Selection item=MonoPrice_Z16_Source label="Zone 6 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z16_Power==ON]
		Slider item=MonoPrice_Z16_Volume visibility=[MonoPrice_Z16_Power==ON]
		Switch item=MonoPrice_Z16_Mute visibility=[MonoPrice_Z16_Power==ON]
		Slider item=MonoPrice_Z16_Treble visibility=[MonoPrice_Z16_Power==ON]
		Slider item=MonoPrice_Z16_Bass visibility=[MonoPrice_Z16_Power==ON]
		Slider item=MonoPrice_Z16_Balance visibility=[MonoPrice_Z16_Power==ON]
   }

RULES:

import java.util.regex.Matcher
import java.util.regex.Pattern

var String zone
   
rule "MonoPrice Status on command change"
when      
        Item MonoPrice_Status changed or
        System started
then                      
        var String MonoPrice_ResponseExpr = ">(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)"
        var Pattern pattern = null 
        var Matcher matcher = null
        pattern = Pattern::compile(MonoPrice_ResponseExpr)
        matcher = pattern.matcher(AudioMTX.toString)
        matcher.find()
        Thread::sleep(300)
        for(var int i = 1; i <= gMonoPriceStatus.members.size; i=i+1){
        postUpdate("MonoPrice_Status_G"+i, matcher.group(i))
    }        
end

rule "Power (G3) Update"
when
	Item MonoPrice_Status_G3 received update 
then	
	if(MonoPrice_Status_G3.state == "01"){
        val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Power", "ON")
    }
	else {	
  	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Power", "OFF")
    }        
end

rule "Mute (G4) Update"
when
	Item MonoPrice_Status_G4 received update 
then	
	if(MonoPrice_Status_G4.state == "01"){
	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Mute", "ON")
    }
	else {	
  	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Mute", "OFF")
    }  
end

rule "Volume (G6) Update"
when
	Item MonoPrice_Status_G6 received update 
then
	val Number vol = ((MonoPrice_Status_G6.state as DecimalType) / 38 * 100).intValue
	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Volume", vol.toString)
	logInfo("MonoPrice", "G6 Volume updated to zone " + num + " Volume level " + vol)      
end

rule "Treble (G7) Update"
when
	Item MonoPrice_Status_G7 received update 
then
	val Number tre = ((MonoPrice_Status_G7.state as DecimalType) / 14 * 100).intValue
	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Treble", tre.toString)
	logInfo("MonoPrice", "G7 Treble updated to zone " + num + " Treble level " + tre)     
end

rule "Bass (G8) Update"
when
	Item MonoPrice_Status_G8 received update 
then
	val Number bas = ((MonoPrice_Status_G8.state as DecimalType) / 14 * 100).intValue 
	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Bass", bas.toString)
	logInfo("MonoPrice", "G8 Bass updated to zone " + num + " Bass level " + bas)     
end

rule "Balance (G9) Update"
when
	Item MonoPrice_Status_G9 received update 
then
	val Number bal = ((MonoPrice_Status_G9.state as DecimalType) / 20 * 100).intValue
	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Balance", bal.toString)
	logInfo("MonoPrice", "G9 Balance updated to zone " + num + " Balance " + bal)     
end

rule "Source (G10) Update"
when
	Item MonoPrice_Status_G10 received update 
then
	val sor = MonoPrice_Status_G10.state
        val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Source", sor.toString)
	logInfo("MonoPrice", "G10 Source updated to zone "+ num + " Source Number " + sor)         
end

rule "Refresh Switch" //used during system startup, cycle through each zone
when
        Item MonoPrice_Refresh received command ON or
        System started
then 
       for(var int i=11; i <= 16; i = i+1){
        AudioMTX.sendCommand("?"+i+"\r")
        Thread::sleep(600)
        MonoPrice_Status.postUpdate(AudioMTX.state.toString.trim)
    }
        logInfo("Monoprice", "Monoprice Amp items refreshed")
end 

rule "MonoPrice Zone Power"
when
        Item MonoPrice_Z11_Power received command or
        Item MonoPrice_Z12_Power received command or
        Item MonoPrice_Z13_Power received command or
        Item MonoPrice_Z14_Power received command or
        Item MonoPrice_Z15_Power received command or
        Item MonoPrice_Z16_Power received command
then
        Thread::sleep(100)
        val power = triggeringItem as SwitchItem 
        switch power {	
        	case MonoPrice_Z11_Power: zone ="1"
        	case MonoPrice_Z12_Power: zone ="2"
        	case MonoPrice_Z13_Power: zone ="3"
        	case MonoPrice_Z14_Power: zone ="4"
        	case MonoPrice_Z15_Power: zone ="5"
        	case MonoPrice_Z16_Power: zone ="6"
        }	        	
        if(receivedCommand==ON){
                AudioMTX.sendCommand("<1" + zone + "PR01\r")
                logInfo("MonoPrice", "Zone "+ zone + " Power ON")
        }
        else if(receivedCommand==OFF){
                AudioMTX.sendCommand("<1" + zone + "PR00\r")
                logInfo("MonoPrice", "Zone " + zone + " Power OFF")
        }
        AudioMTX.sendCommand("?1" + zone + "\r")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX.state.toString.trim)
end

rule "MonoPrice Zone Volume"
when
        Item MonoPrice_Z11_Volume received command or
        Item MonoPrice_Z12_Volume received command or
        Item MonoPrice_Z13_Volume received command or
        Item MonoPrice_Z14_Volume received command or
        Item MonoPrice_Z15_Volume received command or
        Item MonoPrice_Z16_Volume received command
then
	Thread::sleep(100)
        val volume = triggeringItem as DimmerItem
        var Number volume_z
        switch volume {	
        	case MonoPrice_Z11_Volume: zone ="1"
        	case MonoPrice_Z12_Volume: zone ="2"
        	case MonoPrice_Z13_Volume: zone ="3"
        	case MonoPrice_Z14_Volume: zone ="4"
        	case MonoPrice_Z15_Volume: zone ="5"
        	case MonoPrice_Z16_Volume: zone ="6"
        }       
        if(receivedCommand instanceof PercentType) {           
                volume_z = receivedCommand
                volume_z = (volume_z * 38 / 100).intValue             
        } else {
                        if(receivedCommand==INCREASE)                   
                        {
                                volume_z = volume_z + 1
                        }
                        if(receivedCommand==DECREASE)
                        {
                                volume_z = volume_z - 1
                        }
                }
        if (volume_z>9) {AudioMTX.sendCommand("<1" + zone + "VO" + volume_z.toString + "\r") }    
        else {AudioMTX.sendCommand("<1" + zone + "VO0" + volume_z.toString + "\r") }
        volume.postUpdate((volume_z / 38 * 100).intValue) 
        AudioMTX.sendCommand("?1" + zone + "\r")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX.state.toString.trim)                             
end

rule "MonoPrice Zone Mute"
when
        Item MonoPrice_Z11_Mute received command or
        Item MonoPrice_Z12_Mute received command or
        Item MonoPrice_Z13_Mute received command or
        Item MonoPrice_Z14_Mute received command or
        Item MonoPrice_Z15_Mute received command or
        Item MonoPrice_Z16_Mute received command
then
        Thread::sleep(100)
        val mute = triggeringItem as SwitchItem    
        switch mute {	
        	case MonoPrice_Z11_Mute: zone ="1"
        	case MonoPrice_Z12_Mute: zone ="2"
        	case MonoPrice_Z13_Mute: zone ="3"
        	case MonoPrice_Z14_Mute: zone ="4"
        	case MonoPrice_Z15_Mute: zone ="5"
        	case MonoPrice_Z16_Mute: zone ="6"
        }	       	
        if(receivedCommand==ON){
                AudioMTX.sendCommand("<1" + zone + "MU01\r")
                logInfo("MonoPrice", "Zone "+ zone + " Mute ON")
        }
        else if(receivedCommand==OFF){
                AudioMTX.sendCommand("<1" + zone + "MU00\r")
                logInfo("MonoPrice", "Zone " + zone + " Mute OFF")
        }                                  
        AudioMTX.sendCommand("?1" + zone + "\r")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX.state.toString.trim)
end

rule "MonoPrice Zone Source"
when
        Item MonoPrice_Z11_Source received command or
        Item MonoPrice_Z12_Source received command or
        Item MonoPrice_Z13_Source received command or
        Item MonoPrice_Z14_Source received command or
        Item MonoPrice_Z15_Source received command or
        Item MonoPrice_Z16_Source received command
then
        Thread::sleep(100)
        val source = triggeringItem as NumberItem 
        switch source {	
        	case MonoPrice_Z11_Source: zone ="1"
        	case MonoPrice_Z12_Source: zone ="2"
        	case MonoPrice_Z13_Source: zone ="3"
        	case MonoPrice_Z14_Source: zone ="4"
        	case MonoPrice_Z15_Source: zone ="5"
        	case MonoPrice_Z16_Source: zone ="6"
        }	
        AudioMTX.sendCommand("<1" + zone + "CH0" + source.state.toString + "\r")
        AudioMTX.sendCommand("?1" + zone + "\r")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX.state.toString.trim)
end

rule "MonoPrice Zone Tone"
when
        Item MonoPrice_Z11_Treble received command or
        Item MonoPrice_Z12_Treble received command or
        Item MonoPrice_Z13_Treble received command or
        Item MonoPrice_Z14_Treble received command or
        Item MonoPrice_Z15_Treble received command or
        Item MonoPrice_Z16_Treble received command or
        Item MonoPrice_Z11_Bass received command or
        Item MonoPrice_Z12_Bass received command or
        Item MonoPrice_Z13_Bass received command or
        Item MonoPrice_Z14_Bass received command or
        Item MonoPrice_Z15_Bass received command or
        Item MonoPrice_Z16_Bass received command
then
        var Number toneslider_z
        var String tone_z
        Thread::sleep(100)
        val tone = triggeringItem as DimmerItem    
        switch tone {	
        	case MonoPrice_Z11_Treble: {
        	zone ="1" 
        	tone_z = "1TR" 
        	}
        	case MonoPrice_Z12_Treble: {
        	zone ="2" 
        	tone_z = "2TR"
        	}
        	case MonoPrice_Z13_Treble: {
        	zone ="3" 
        	tone_z = "3TR"
        	}
        	case MonoPrice_Z14_Treble: {
        	zone ="4"
        	tone_z = "4TR"
        	}
        	case MonoPrice_Z15_Treble: {
        	zone ="5"
        	tone_z = "5TR"
        	}
        	case MonoPrice_Z16_Treble: {
        	zone ="6"
        	tone_z = "6TR"
        	}
        	case MonoPrice_Z11_Bass: {
        	zone ="1"
        	tone_z = "1BS"
        	}
        	case MonoPrice_Z12_Bass: {
        	zone ="2"
        	tone_z = "2BS"
        	}
        	case MonoPrice_Z13_Bass: {
        	zone ="3"
        	tone_z = "3BS"
        	}
        	case MonoPrice_Z14_Bass: {
        	zone ="4"
        	tone_z = "4BS"
        	}
        	case MonoPrice_Z15_Bass:{ 
        	zone ="5"
        	tone_z = "5BS"
        	}
        	case MonoPrice_Z16_Bass: {
        	zone ="6"
        	tone_z = "6BS"
        	}
        }	
        if(receivedCommand instanceof PercentType) {                    
                toneslider_z = receivedCommand
                toneslider_z = (toneslider_z * 14 / 100).intValue             
        } else {
                        if(receivedCommand==INCREASE)                   
                        {
                                toneslider_z = toneslider_z + 1
                        }
                        if(receivedCommand==DECREASE)
                        {
                                toneslider_z = toneslider_z - 1
                        }
                }
        if (toneslider_z>9) {AudioMTX.sendCommand("<1" + tone_z + toneslider_z.toString + "\r") }    
        else {AudioMTX.sendCommand("<1" + tone_z + "0" + toneslider_z + "\r") }
        tone.postUpdate((toneslider_z / 14 * 100).intValue)                              
        AudioMTX.sendCommand("<1" + zone + "\r")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX.state.toString.trim)
end

rule "MonoPrice Zone Balance"
when
        Item MonoPrice_Z11_Balance received command or
        Item MonoPrice_Z12_Balance received command or
        Item MonoPrice_Z13_Balance received command or
        Item MonoPrice_Z14_Balance received command or
        Item MonoPrice_Z15_Balance received command or
        Item MonoPrice_Z16_Balance received command
then
        var Number balance_z
        val balance = triggeringItem as DimmerItem 
        Thread::sleep(100)
        switch balance {	
        	case MonoPrice_Z11_Balance: zone ="1"
        	case MonoPrice_Z12_Balance: zone ="2"
        	case MonoPrice_Z13_Balance: zone ="3"
        	case MonoPrice_Z14_Balance: zone ="4"
        	case MonoPrice_Z15_Balance: zone ="5"
        	case MonoPrice_Z16_Balance: zone ="6"
        }
        
        if(receivedCommand instanceof PercentType) {                    
                balance_z = receivedCommand
                balance_z = (balance_z * 20 / 100).intValue             
        } else {
                        if(receivedCommand==INCREASE)                  
                        {
                                balance_z = balance_z + 1
                        }
                        if(receivedCommand==DECREASE)
                        {
                                balance_z = balance_z - 1
                        }
                }
        if (balance_z>9) {AudioMTX.sendCommand("<1" + zone + "BL" + balance_z.toString + "\r") }   
        else {AudioMTX.sendCommand("<1" + zone + "BL0" + balance_z.toString + "\r") }
        balance.postUpdate((balance_z / 20 * 100).intValue)                              
        AudioMTX.sendCommand("?1" + zone + "\r")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX.state.toString.trim)
end
4 Likes

I have learn so much with this post!
Thank you very much!

Thanks for the response, i’m glad it helped you. I’ve learned a lot just writing this set of rules, but I’m a new user of OH, and I know there’s much that could be improved. For example, if I knew how to use arrays in the rules, I wouldn’t have to make a unique item/rule for each amp zone. For now, this works well for me, and I’ve got other “todos” in OH for now :slight_smile:

Hey, I’m just looking through your scripts. Just for clarity, on line 2 of your items file, you’ve got

{ serial="/dev/ttyUSB1@9600" }

which I assume means you’re using a USB to Serial cable at 9600 baud.

Do you happen to know what I would change it to using GlobalCache iTach Flex @ IP 172.16.1.100 ?

@jroffey

I modified @bartus great work to adapt to using a GlobalCache iTach Flex. I also modified the script to use group persistence, which greatly reduced the repetitive code for each zone.

Setup notes:

  • You must set up mapDB persistence

  • Refer to Design Pattern: Working with Groups in a Rule for more information
    Design Pattern: Working with Groups in Rules

  • Set up your Monoprice GlobalCache iTach Flex Thing for “Direct” serial communication (no .map file required)

  • Enable 2-way communication for the iTach thing using “%0D%0A” as the End-of-message Delimiter

Item file:

Group gZonePowerSwitch (all)
Group gZoneVolume (all)
Group gZoneMuteSwitch (all)
Group gZoneTone (all)
Group gZoneBalance (all)
Group gZoneSource (all)
Group gMonoPriceStatus (all)

String AudioMTX             "Audio Matrix"           { channel="globalcache:itachFlex:audiomtx:sl-m1#c1-direct" }
String AudioMTX_Receive     "Audio Matrix Receive"   { channel="globalcache:itachFlex:audiomtx:sl-m1#c1-receive" }
Switch MonoPrice_Refresh    "Refresh String"
//String MonoPrice_Receive_String "Received Response"
String MonoPrice_Status     "MonoPrice Status [%s]"
String MonoPrice_Status_G1  "MP G1 (Zone) [%s]"             (gMonoPriceStatus)
String MonoPrice_Status_G2  "MP G2 (PA Status) [%s]"        (gMonoPriceStatus)
String MonoPrice_Status_G3  "MP G3 (Pwr Status) [%s]"       (gMonoPriceStatus)
String MonoPrice_Status_G4  "MP G4 (Mute Status) [%s]"      (gMonoPriceStatus)
String MonoPrice_Status_G5  "MP G5 (DT Status) [%s]"        (gMonoPriceStatus)
Number MonoPrice_Status_G6  "MP G6 (Volume Status) [%.0f]"  (gMonoPriceStatus)
Number MonoPrice_Status_G7  "MP G7 (Treble Status) [%.0f]"  (gMonoPriceStatus)
Number MonoPrice_Status_G8  "MP G8 (Bass Status) [%.0f]"    (gMonoPriceStatus)
Number MonoPrice_Status_G9  "MP G9 (Balance) [%.0f]"        (gMonoPriceStatus)
Number MonoPrice_Status_G10 "MP G10 (Src Status) [%.0f]"    (gMonoPriceStatus)
String MonoPrice_Status_G11 "MP G11 (Keypad Status) [%s]"   (gMonoPriceStatus)
String MonoPrice_Z11         "Zone 1 Status [%s]"
String MonoPrice_Z12         "Zone 2 Status [%s]"
String MonoPrice_Z13         "Zone 3 Status [%s]"
String MonoPrice_Z14         "Zone 4 Status [%s]"
String MonoPrice_Z15         "Zone 5 Status [%s]"
String MonoPrice_Z16         "Zone 6 Status [%s]"
Switch MonoPrice_Z11_Power   "Zone 1 Power [%s]"        <switch>		 (gZonePowerSwitch)
Switch MonoPrice_Z12_Power   "Zone 2 Power [%s]"        <switch>		 (gZonePowerSwitch)
Switch MonoPrice_Z13_Power   "Zone 3 Power [%s]"        <switch>		 (gZonePowerSwitch)
Switch MonoPrice_Z14_Power   "Zone 4 Power [%s]"        <switch>		 (gZonePowerSwitch)
Switch MonoPrice_Z15_Power   "Zone 5 Power [%s]"        <switch>		 (gZonePowerSwitch)
Switch MonoPrice_Z16_Power   "Zone 6 Power [%s]"        <switch> 	 (gZonePowerSwitch)
Switch MonoPrice_Z11_Mute    "Zone 1 Mute [%s]"   	   <mute> 		 (gZoneMuteSwitch)
Switch MonoPrice_Z12_Mute    "Zone 2 Mute [%s]"    	   <mute> 		 (gZoneMuteSwitch)
Switch MonoPrice_Z13_Mute    "Zone 3 Mute [%s]"    	   <mute> 		 (gZoneMuteSwitch)
Switch MonoPrice_Z14_Mute    "Zone 4 Mute [%s]"    	   <mute> 		 (gZoneMuteSwitch)
Switch MonoPrice_Z15_Mute    "Zone 5 Mute [%s]"     	   <mute> 		 (gZoneMuteSwitch)
Switch MonoPrice_Z16_Mute    "Zone 6 Mute [%s]"    	   <mute> 		 (gZoneMuteSwitch)
Dimmer MonoPrice_Z11_Volume  "Zone 1 Volume [%02d %%]"  <soundvolume> (gZoneVolume)
Dimmer MonoPrice_Z12_Volume  "Zone 2 Volume [%02d %%]"  <soundvolume> (gZoneVolume)
Dimmer MonoPrice_Z13_Volume  "Zone 3 Volume [%02d %%]"  <soundvolume> (gZoneVolume)
Dimmer MonoPrice_Z14_Volume  "Zone 4 Volume [%02d %%]"  <soundvolume> (gZoneVolume)
Dimmer MonoPrice_Z15_Volume  "Zone 5 Volume [%02d %%]"  <soundvolume> (gZoneVolume)
Dimmer MonoPrice_Z16_Volume  "Zone 6 Volume [%02d %%]"  <soundvolume> (gZoneVolume)
Dimmer MonoPrice_Z11_Treble  "Zone 1 Treble [%02d %%]"  <treble>      (gZoneTone)
Dimmer MonoPrice_Z12_Treble  "Zone 2 Treble [%02d %%]"  <treble>      (gZoneTone)
Dimmer MonoPrice_Z13_Treble  "Zone 3 Treble [%02d %%]"  <treble>      (gZoneTone)
Dimmer MonoPrice_Z14_Treble  "Zone 4 Treble [%02d %%]"  <treble>      (gZoneTone)
Dimmer MonoPrice_Z15_Treble  "Zone 5 Treble [%02d %%]"  <treble>      (gZoneTone) 
Dimmer MonoPrice_Z16_Treble  "Zone 6 Treble [%02d %%]"  <treble>	     (gZoneTone)
Dimmer MonoPrice_Z11_Bass    "Zone 1 Bass [%02d %%]"  	<bass>		 (gZoneTone)
Dimmer MonoPrice_Z12_Bass    "Zone 2 Bass [%02d %%]"  	<bass>		 (gZoneTone)
Dimmer MonoPrice_Z13_Bass    "Zone 3 Bass [%02d %%]"   	<bass>		 (gZoneTone)
Dimmer MonoPrice_Z14_Bass    "Zone 4 Bass [%02d %%]"   	<bass>		 (gZoneTone)
Dimmer MonoPrice_Z15_Bass    "Zone 5 Bass [%02d %%]"    	<bass>		 (gZoneTone)
Dimmer MonoPrice_Z16_Bass    "Zone 6 Bass [%02d %%]"    	<bass>		 (gZoneTone)
Dimmer MonoPrice_Z11_Balance "Zone 1 Balance [%02d %%]" 	<control>	 (gZoneBalance)
Dimmer MonoPrice_Z12_Balance "Zone 2 Balance [%02d %%]" 	<control>	 (gZoneBalance)
Dimmer MonoPrice_Z13_Balance "Zone 3 Balance [%02d %%]" 	<control>	 (gZoneBalance)
Dimmer MonoPrice_Z14_Balance "Zone 4 Balance [%02d %%]" 	<control>	 (gZoneBalance)
Dimmer MonoPrice_Z15_Balance "Zone 5 Balance [%02d %%]" 	<control>	 (gZoneBalance)
Dimmer MonoPrice_Z16_Balance "Zone 6 Balance [%02d %%]" 	<control>	 (gZoneBalance)
Number MonoPrice_Z11_Source  "Zone 1 Source [%.0f]" 	<control>		 (gZoneSource)
Number MonoPrice_Z12_Source  "Zone 2 Source [%.0f]" 	<control>		 (gZoneSource)
Number MonoPrice_Z13_Source  "Zone 3 Source [%.0f]"	<control>		 (gZoneSource)
Number MonoPrice_Z14_Source  "Zone 4 Source [%.0f]"	<control>		 (gZoneSource)
Number MonoPrice_Z15_Source  "Zone 5 Source [%.0f]"	<control>		 (gZoneSource)
Number MonoPrice_Z16_Source  "Zone 6 Source [%.0f]"	<control>		 (gZoneSource)
//Not using the following items but here for complete reference
//String MonoPrice_Z1_PA      "Zone 1 PA Status [%s]"
//String MonoPrice_Z2_PA      "Zone 2 PA Status [%s]"
//String MonoPrice_Z3_PA      "Zone 3 PA Status [%s]"
//String MonoPrice_Z4_PA      "Zone 4 PA Status [%s]"
//String MonoPrice_Z5_PA      "Zone 5 PA Status [%s]"
//String MonoPrice_Z6_PA      "Zone 6 PA Status [%s]"
//String MonoPrice_Z1_KeyPad  "Zone 1 Key Pad Status [%s]"
//String MonoPrice_Z2_KeyPad  "Zone 2 Key Pad Status [%s]"
//String MonoPrice_Z3_KeyPad  "Zone 3 Key Pad Status [%s]"
//String MonoPrice_Z4_KeyPad  "Zone 4 Key Pad Status [%s]"
//String MonoPrice_Z5_KeyPad  "Zone 5 Key Pad Status [%s]"
//String MonoPrice_Z6_KeyPad  "Zone 6 Key Pad Status [%s]"
//Switch MonoPrice_Z1_DT      "Zone 1 Do Not Disturb [%s]"
//Switch MonoPrice_Z2_DT      "Zone 2 Do Not Disturb [%s]"
//Switch MonoPrice_Z3_DT      "Zone 3 Do Not Disturb [%s]"
//Switch MonoPrice_Z4_DT      "Zone 4 Do Not Disturb [%s]"
//Switch MonoPrice_Z5_DT      "Zone 5 Do Not Disturb [%s]"
//Switch MonoPrice_Z6_DT      "Zone 6 Do Not Disturb [%s]"

Rule:

import java.util.regex.Matcher
import java.util.regex.Pattern

var String zone
   
rule "MonoPrice Status on command change"
when      
        Item MonoPrice_Status changed or
        System started
then                      
        var String MonoPrice_ResponseExpr = "%23%3E(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)%0D%0D%0A"
        var Pattern pattern = null 
        var Matcher matcher = null
        pattern = Pattern::compile(MonoPrice_ResponseExpr)
        matcher = pattern.matcher(AudioMTX_Receive.toString)
        matcher.find()
        Thread::sleep(300)
        for(var int i = 1; i <= gMonoPriceStatus.members.size; i=i+1){
        postUpdate("MonoPrice_Status_G"+i, matcher.group(i))
    }        
end

rule "Power (G3) Update"
when
	Item MonoPrice_Status_G3 received update 
then	
	if(MonoPrice_Status_G3.state == "01"){
        val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Power", "ON")
    }
	else {	
  	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Power", "OFF")
    }        
end

rule "Mute (G4) Update"
when
	Item MonoPrice_Status_G4 received update 
then	
	if(MonoPrice_Status_G4.state == "01"){
	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Mute", "ON")
    }
	else {	
  	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Mute", "OFF")
    }  
end

rule "Volume (G6) Update"
when
	Item MonoPrice_Status_G6 received update 
then
	val Number vol = ((MonoPrice_Status_G6.state as DecimalType) / 38 * 100).intValue
	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Volume", vol.toString)
	logInfo("MonoPrice", "G6 Volume updated to zone " + num + " Volume level " + vol)      
end

rule "Treble (G7) Update"
when
	Item MonoPrice_Status_G7 received update 
then
	val Number tre = ((MonoPrice_Status_G7.state as DecimalType) / 14 * 100).intValue
	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Treble", tre.toString)
	logInfo("MonoPrice", "G7 Treble updated to zone " + num + " Treble level " + tre)     
end

rule "Bass (G8) Update"
when
	Item MonoPrice_Status_G8 received update 
then
	val Number bas = ((MonoPrice_Status_G8.state as DecimalType) / 14 * 100).intValue 
	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Bass", bas.toString)
	logInfo("MonoPrice", "G8 Bass updated to zone " + num + " Bass level " + bas)     
end

rule "Balance (G9) Update"
when
	Item MonoPrice_Status_G9 received update 
then
	val Number bal = ((MonoPrice_Status_G9.state as DecimalType) / 20 * 100).intValue
	val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Balance", bal.toString)
	logInfo("MonoPrice", "G9 Balance updated to zone " + num + " Balance " + bal)     
end

rule "Source (G10) Update"
when
	Item MonoPrice_Status_G10 received update 
then
	val sor = MonoPrice_Status_G10.state
        val int num = Integer::parseInt(MonoPrice_Status_G1.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Source", sor.toString)
	logInfo("MonoPrice", "G10 Source updated to zone "+ num + " Source Number " + sor)         
end

rule "Refresh Switch" //used during system startup, cycle through each zone
when
        Item MonoPrice_Refresh received command ON or
        System started
then 
       for(var int i=11; i <= 16; i = i+1){
        AudioMTX.sendCommand("%3f"+i+"%0D%0A")
        Thread::sleep(600)
        MonoPrice_Status.postUpdate(AudioMTX_Receive.state.toString.trim)
    }
        logInfo("Monoprice", "Monoprice Amp items refreshed")
end 

rule "MonoPrice Zone Power"
when
        Item MonoPrice_Z11_Power received command or
        Item MonoPrice_Z12_Power received command or
        Item MonoPrice_Z13_Power received command or
        Item MonoPrice_Z14_Power received command or
        Item MonoPrice_Z15_Power received command or
        Item MonoPrice_Z16_Power received command
then
        Thread::sleep(100)
        val power = gZonePowerSwitch.members.filter[t|t.lastUpdate("mapdb") !== null].sortBy[lastUpdate("mapdb")].last as SwitchItem
        val source = gZoneSource.members.filter[t|t.lastUpdate("mapdb") !== null].sortBy[lastUpdate("mapdb")].last as NumberItem
        switch power {	
        	case MonoPrice_Z11_Power: zone ="1"
        	case MonoPrice_Z12_Power: zone ="2"
        	case MonoPrice_Z13_Power: zone ="3"
        	case MonoPrice_Z14_Power: zone ="4"
        	case MonoPrice_Z15_Power: zone ="5"
        	case MonoPrice_Z16_Power: zone ="6"
        }	        	
        if(receivedCommand==ON){
                AudioMTX.sendCommand("%3C1" + zone + "PR01%0D%0A")
                logInfo("MonoPrice", "Zone "+ zone + " Power ON")
                
                if (source.state.toString == "2") {
                zone2Power.sendCommand(ON)
                }
        }
        else if(receivedCommand==OFF){
                AudioMTX.sendCommand("%3C1" + zone + "PR00%0D%0A")
                logInfo("MonoPrice", "Zone " + zone + " Power OFF")
        }
        AudioMTX.sendCommand("%3f1" + zone + "%0D%0A")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX_Receive.state.toString.trim)
end

rule "MonoPrice Zone Volume"
when
        Item MonoPrice_Z11_Volume received command or
        Item MonoPrice_Z12_Volume received command or
        Item MonoPrice_Z13_Volume received command or
        Item MonoPrice_Z14_Volume received command or
        Item MonoPrice_Z15_Volume received command or
        Item MonoPrice_Z16_Volume received command
then
	Thread::sleep(100)
        val volume = gZoneVolume.members.filter[t|t.lastUpdate("mapdb") !== null].sortBy[lastUpdate("mapdb")].last as DimmerItem
        var Number volume_z
        switch volume {	
        	case MonoPrice_Z11_Volume: zone ="1"
        	case MonoPrice_Z12_Volume: zone ="2"
        	case MonoPrice_Z13_Volume: zone ="3"
        	case MonoPrice_Z14_Volume: zone ="4"
        	case MonoPrice_Z15_Volume: zone ="5"
        	case MonoPrice_Z16_Volume: zone ="6"
        }       
        if(receivedCommand instanceof PercentType) {           
                volume_z = receivedCommand
                volume_z = (volume_z * 38 / 100).intValue             
        } else {
                        if(receivedCommand==INCREASE)                   
                        {
                                volume_z = volume_z + 1
                        }
                        if(receivedCommand==DECREASE)
                        {
                                volume_z = volume_z - 1
                        }
                }
        if (volume_z>9) {AudioMTX.sendCommand("%3C1" + zone + "VO" + volume_z.toString + "%0D%0A") }    
        else {AudioMTX.sendCommand("%3C1" + zone + "VO0" + volume_z.toString + "%0D%0A") }
        volume.postUpdate((volume_z / 38 * 100).intValue) 
        AudioMTX.sendCommand("%3f1" + zone + "%0D%0A")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX_Receive.state.toString.trim)                             
end

rule "MonoPrice Zone Mute"
when
        Item MonoPrice_Z11_Mute received command or
        Item MonoPrice_Z12_Mute received command or
        Item MonoPrice_Z13_Mute received command or
        Item MonoPrice_Z14_Mute received command or
        Item MonoPrice_Z15_Mute received command or
        Item MonoPrice_Z16_Mute received command
then
        Thread::sleep(100)
        val mute = gZoneMuteSwitch.members.filter[t|t.lastUpdate("mapdb") !== null].sortBy[lastUpdate("mapdb")].last as SwitchItem       
        switch mute {	
        	case MonoPrice_Z11_Mute: zone ="1"
        	case MonoPrice_Z12_Mute: zone ="2"
        	case MonoPrice_Z13_Mute: zone ="3"
        	case MonoPrice_Z14_Mute: zone ="4"
        	case MonoPrice_Z15_Mute: zone ="5"
        	case MonoPrice_Z16_Mute: zone ="6"
        }	       	
        if(receivedCommand==ON){
                AudioMTX.sendCommand("%3C1" + zone + "MU01%0D%0A")
                logInfo("MonoPrice", "Zone "+ zone + " Mute ON")
        }
        else if(receivedCommand==OFF){
                AudioMTX.sendCommand("%3C1" + zone + "MU00%0D%0A")
                logInfo("MonoPrice", "Zone " + zone + " Mute OFF")
        }                                  
        AudioMTX.sendCommand("%3f1" + zone + "%0D%0A")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX_Receive.state.toString.trim)
end

rule "MonoPrice Zone Source"
when
        Item MonoPrice_Z11_Source received command or
        Item MonoPrice_Z12_Source received command or
        Item MonoPrice_Z13_Source received command or
        Item MonoPrice_Z14_Source received command or
        Item MonoPrice_Z15_Source received command or
        Item MonoPrice_Z16_Source received command
then
        Thread::sleep(100)
        val source = gZoneSource.members.filter[t|t.lastUpdate("mapdb") !== null].sortBy[lastUpdate("mapdb")].last as NumberItem        
        switch source {	
        	case MonoPrice_Z11_Source: zone ="1"
        	case MonoPrice_Z12_Source: zone ="2"
        	case MonoPrice_Z13_Source: zone ="3"
        	case MonoPrice_Z14_Source: zone ="4"
        	case MonoPrice_Z15_Source: zone ="5"
        	case MonoPrice_Z16_Source: zone ="6"
        }	
        AudioMTX.sendCommand("%3C1" + zone + "CH0" + source.state.toString + "%0D%0A")
        if (source.state.toString == "2") {
        zone2Power.sendCommand(ON)
        }
        AudioMTX.sendCommand("%3f1" + zone + "%0D%0A")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX_Receive.state.toString.trim)
end

rule "MonoPrice Zone Tone"
when
        Item MonoPrice_Z11_Treble received command or
        Item MonoPrice_Z12_Treble received command or
        Item MonoPrice_Z13_Treble received command or
        Item MonoPrice_Z14_Treble received command or
        Item MonoPrice_Z15_Treble received command or
        Item MonoPrice_Z16_Treble received command or
        Item MonoPrice_Z11_Bass received command or
        Item MonoPrice_Z12_Bass received command or
        Item MonoPrice_Z13_Bass received command or
        Item MonoPrice_Z14_Bass received command or
        Item MonoPrice_Z15_Bass received command or
        Item MonoPrice_Z16_Bass received command
then
        var Number toneslider_z
        var String tone_z
        Thread::sleep(100)
        val tone = gZoneTone.members.filter[t|t.lastUpdate("mapdb") !== null].sortBy[lastUpdate("mapdb")].last as DimmerItem       
        switch tone {	
        	case MonoPrice_Z11_Treble: {
        	zone ="1" 
        	tone_z = "1TR" 
        	}
        	case MonoPrice_Z12_Treble: {
        	zone ="2" 
        	tone_z = "2TR"
        	}
        	case MonoPrice_Z13_Treble: {
        	zone ="3" 
        	tone_z = "3TR"
        	}
        	case MonoPrice_Z14_Treble: {
        	zone ="4"
        	tone_z = "4TR"
        	}
        	case MonoPrice_Z15_Treble: {
        	zone ="5"
        	tone_z = "5TR"
        	}
        	case MonoPrice_Z16_Treble: {
        	zone ="6"
        	tone_z = "6TR"
        	}
        	case MonoPrice_Z11_Bass: {
        	zone ="1"
        	tone_z = "1BS"
        	}
        	case MonoPrice_Z12_Bass: {
        	zone ="2"
        	tone_z = "2BS"
        	}
        	case MonoPrice_Z13_Bass: {
        	zone ="3"
        	tone_z = "3BS"
        	}
        	case MonoPrice_Z14_Bass: {
        	zone ="4"
        	tone_z = "4BS"
        	}
        	case MonoPrice_Z15_Bass:{ 
        	zone ="5"
        	tone_z = "5BS"
        	}
        	case MonoPrice_Z16_Bass: {
        	zone ="6"
        	tone_z = "6BS"
        	}
        }	
        if(receivedCommand instanceof PercentType) {                    
                toneslider_z = receivedCommand
                toneslider_z = (toneslider_z * 14 / 100).intValue             
        } else {
                        if(receivedCommand==INCREASE)                   
                        {
                                toneslider_z = toneslider_z + 1
                        }
                        if(receivedCommand==DECREASE)
                        {
                                toneslider_z = toneslider_z - 1
                        }
                }
        if (toneslider_z>9) {AudioMTX.sendCommand("%3C1" + tone_z + toneslider_z.toString + "%0D%0A") }    
        else {AudioMTX.sendCommand("%3C1" + tone_z + "0" + toneslider_z + "%0D%0A") }
        tone.postUpdate((toneslider_z / 14 * 100).intValue)                              
        AudioMTX.sendCommand("%3f1" + zone + "%0D%0A")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX_Receive.state.toString.trim)
end

rule "MonoPrice Zone Balance"
when
        Item MonoPrice_Z11_Balance received command or
        Item MonoPrice_Z12_Balance received command or
        Item MonoPrice_Z13_Balance received command or
        Item MonoPrice_Z14_Balance received command or
        Item MonoPrice_Z15_Balance received command or
        Item MonoPrice_Z16_Balance received command
then
        var Number balance_z
        val balance = gZoneBalance.members.filter[t|t.lastUpdate("mapdb") !== null].sortBy[lastUpdate("mapdb")].last as DimmerItem
        Thread::sleep(100)
        switch balance {	
        	case MonoPrice_Z11_Balance: zone ="1"
        	case MonoPrice_Z12_Balance: zone ="2"
        	case MonoPrice_Z13_Balance: zone ="3"
        	case MonoPrice_Z14_Balance: zone ="4"
        	case MonoPrice_Z15_Balance: zone ="5"
        	case MonoPrice_Z16_Balance: zone ="6"
        }
        
        if(receivedCommand instanceof PercentType) {                    
                balance_z = receivedCommand
                balance_z = (balance_z * 20 / 100).intValue             
        } else {
                        if(receivedCommand==INCREASE)                  
                        {
                                balance_z = balance_z + 1
                        }
                        if(receivedCommand==DECREASE)
                        {
                                balance_z = balance_z - 1
                        }
                }
        if (balance_z>9) {AudioMTX.sendCommand("%3C1" + zone + "BL" + balance_z.toString + "%0D%0A") }   
        else {AudioMTX.sendCommand("%3C1" + zone + "BL0" + balance_z.toString + "%0D%0A") }
        balance.postUpdate((balance_z / 20 * 100).intValue)                              
        AudioMTX.sendCommand("%3f1" + zone + "%0D%0A")                                            
        Thread::sleep(300)
        MonoPrice_Status.postUpdate(AudioMTX_Receive.state.toString.trim)
end

Sitemap:

Frame label="MonoPrice" {
		Switch item=MonoPrice_Refresh
		//Text item=AudioMTX_Receive
		//Text item=MonoPrice_Status
		//Text item=MonoPrice_Status_G1
		// Text item=MonoPrice_Status_G2
		// Text item=MonoPrice_Status_G3
		// Text item=MonoPrice_Status_G4
		// Text item=MonoPrice_Status_G5
		// Text item=MonoPrice_Status_G6
		// Text item=MonoPrice_Status_G7
	    // Text item=MonoPrice_Status_G8
		// Text item=MonoPrice_Status_G9
		// Text item=MonoPrice_Status_G10
		// Text item=MonoPrice_Status_G11
		Text item=MonoPrice_Z11
		Switch item=MonoPrice_Z11_Power
		Selection item=MonoPrice_Z11_Source label="Zone 1 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z11_Power==ON]
		Slider item=MonoPrice_Z11_Volume visibility=[MonoPrice_Z11_Power==ON]
		Switch item=MonoPrice_Z11_Mute visibility=[MonoPrice_Z11_Power==ON]
		Slider item=MonoPrice_Z11_Treble visibility=[MonoPrice_Z11_Power==ON]
		Slider item=MonoPrice_Z11_Bass visibility=[MonoPrice_Z11_Power==ON]
		Slider item=MonoPrice_Z11_Balance visibility=[MonoPrice_Z11_Power==ON]
		Text item=MonoPrice_Z12
		Switch item=MonoPrice_Z12_Power
		Selection item=MonoPrice_Z12_Source label="Zone 2 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z12_Power==ON]
		Slider item=MonoPrice_Z12_Volume visibility=[MonoPrice_Z12_Power==ON]
		Switch item=MonoPrice_Z12_Mute visibility=[MonoPrice_Z12_Power==ON]
		Slider item=MonoPrice_Z12_Treble visibility=[MonoPrice_Z12_Power==ON]
		Slider item=MonoPrice_Z12_Bass visibility=[MonoPrice_Z12_Power==ON]
		Slider item=MonoPrice_Z12_Balance visibility=[MonoPrice_Z12_Power==ON]
		Text item=MonoPrice_Z13
		Switch item=MonoPrice_Z13_Power
		Selection item=MonoPrice_Z13_Source label="Zone 3 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z13_Power==ON]
		Slider item=MonoPrice_Z13_Volume visibility=[MonoPrice_Z13_Power==ON]
		Switch item=MonoPrice_Z13_Mute visibility=[MonoPrice_Z13_Power==ON]
		Slider item=MonoPrice_Z13_Treble visibility=[MonoPrice_Z13_Power==ON]
		Slider item=MonoPrice_Z13_Bass visibility=[MonoPrice_Z13_Power==ON]
		Slider item=MonoPrice_Z13_Balance visibility=[MonoPrice_Z13_Power==ON]
		Text item=MonoPrice_Z14
		Switch item=MonoPrice_Z14_Power
		Selection item=MonoPrice_Z14_Source label="Zone 4 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z14_Power==ON]
		Slider item=MonoPrice_Z14_Volume visibility=[MonoPrice_Z14_Power==ON]
		Switch item=MonoPrice_Z14_Mute visibility=[MonoPrice_Z14_Power==ON]
		Slider item=MonoPrice_Z14_Treble visibility=[MonoPrice_Z14_Power==ON]
		Slider item=MonoPrice_Z14_Bass visibility=[MonoPrice_Z14_Power==ON]
		Slider item=MonoPrice_Z14_Balance visibility=[MonoPrice_Z14_Power==ON]
		Text item=MonoPrice_Z15
		Switch item=MonoPrice_Z15_Power
		Selection item=MonoPrice_Z15_Source label="Zone 5 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z15_Power==ON]
		Slider item=MonoPrice_Z15_Volume visibility=[MonoPrice_Z15_Power==ON]
		Switch item=MonoPrice_Z15_Mute visibility=[MonoPrice_Z15_Power==ON]
		Slider item=MonoPrice_Z15_Treble visibility=[MonoPrice_Z15_Power==ON]
		Slider item=MonoPrice_Z15_Bass visibility=[MonoPrice_Z15_Power==ON]
		Slider item=MonoPrice_Z15_Balance visibility=[MonoPrice_Z15_Power==ON]
		Text item=MonoPrice_Z16
		Switch item=MonoPrice_Z16_Power
		Selection item=MonoPrice_Z16_Source label="Zone 6 Source" mappings=[1=CH1,2=CH2,3=CH3,4=CH4,5=CH5,6=CH6] visibility=[MonoPrice_Z16_Power==ON]
		Slider item=MonoPrice_Z16_Volume visibility=[MonoPrice_Z16_Power==ON]
		Switch item=MonoPrice_Z16_Mute visibility=[MonoPrice_Z16_Power==ON]
		Slider item=MonoPrice_Z16_Treble visibility=[MonoPrice_Z16_Power==ON]
		Slider item=MonoPrice_Z16_Bass visibility=[MonoPrice_Z16_Power==ON]
		Slider item=MonoPrice_Z16_Balance visibility=[MonoPrice_Z16_Power==ON]
   }

Some of the lines may be excessive. I have a 3rd party iPhone app that can control my amp and I have been working on getting the refresh code in OH to read when the 3rd party app changes settings on the amp. For the record, it doesn’t refresh consistently yet but it works well if you only control using OH.

Also, note that several of the lines are commented out. I will often uncomment these line for troubleshooting purposes to see that the serial commands are being sent/received.

Hope this helps.

UPDATED 02/25/2018: Incorporated most of the suggestions from @rikoshak
Also removed much of the delay time from within the rules to speed up responsiveness.

1 Like

Nice code. I’m not posting this to say you should change anything, just some ideas that might help reduce the lines of code a bit.

  1. If you put all your MonoPrice_Satus Items into a Group, you can simplify your “MonoPrice Status on Command change” Rule in one of several ways. I’ll show how using a for loop:
    ...
    matcher.find()
    Thread::sleep(1000)
    for(var int i = 1; i <= MonoPrice_Statuses.members.size; i=i+1){
        postUpdate("MonoPrice_Status_G"+i, matcher.group(i))
    }

You could also apply some of the ideas from Design Pattern: Associated Items

  1. It makes no sense to declare pattern and matcher as global variables if you are going to recreate them every time your rule runs. Either set them to their value where you declare them as a global, or don’t declare them as global and declare them as local to the Rule. This applies to the other gloval variables. They should not be global unless their value needs to be used across multiple executions of the same rule or by more than one rule.

  2. A sleep of one second is starting to push the max you should sleep for in a Rule. I would use a Timer to avoid tieing up the execution thread.

    ...
    matcher.find()
    createTimer(now.plusSeconds(1), [|
        for(var int i = 1; i <= MonoPrice_Statuses.members.size; i=i+1){
            postUpdate("MonoPrice_Status_G"+i, matcher.group(i))
        }
    ]
  1. In your remaining rules it looks like we can apply some of the Associated Items concepts as well to eliminate some of the switch statements:
    if(MonoPrice_Status_G3.state == "01"){
        val int num = Integer::parseInt(MonoPrice_Status_G3.state.toString)
        postUpdate("MonoPrice_Z"+num+"_Power", "ON")
    }
    else {
        ...
  1. “Refresh Switch” can use the same for loop approach above to reduce the lines of code:
    for(var int i=11; i <= 16; i = i+1){
        AudioMTX.sendCommand("%3f"+i+"%0D%0A")
        Thread::sleep(1000)
        MonoPrice_Status.postUpdate(AudioMTX_Receive.state.toString.trim)
    }
  1. Replace all of the Group/Persistence hacks to get the Item that triggered the Rule with the new implicit variable triggeringItem where applicable. I’m not super clear on how all of this works together so it isn’t clear whether this is applicable.

Thanks for posting!

2 Likes

@swamiller , excellent work! This was my first real rule coding project in OH, so I knew there were many possible improvements, but since it “just worked” I put them on backburner. I will definitely look into rewriting the code now to incorporate your improvements…For the problem of not getting external updates (i.e. using a phone app to control the amp and not having current status properly displayed), I think the easiest solution would be to create a cron rule (every minute or so) that will query the amp status. That should be good enough for what we need here.

@rlkoshak , thanks for even more improvements, I need to learn your group design patterns because I have a great use case for them, not only with these rules, but also with all my LED strip controllers (changing brightness, hue, FX,…). I “cheated” by making them all subscribe to the same MQTT command topic, but status returns are still something that needs to be handled separately.

For sleep, 1 second was just a shot in the dark, that took care of some thrashing (the command/status use the same string variable, and there needs to be a delay between sending a command and sending a status request (and another delay to let the amp populate the serial string with status). What would you consider a reasonable delay value in a rule? Of course, the timer example you posted should work just fine, as well.

According to Kai, any sleep more than 300-500 msec is probably too long. So in the “Refresh Switch” rule above, where it sleeps for a total of 16 seconds there could be some major problems if it is ever possible for a few of these rules running at the same time. There is a small number of execution threads available to run Rules and sleeping for a long time ties up one of those threads.

If you truly have a constrained resource, you can try Design Pattern: Gate Keeper. This separates the communications to your constrained resource from the rules that need to make the communication so that you can manage the timing between messages. This is particularly important if there are multiple rules that need access to the same constrained resource.

1 Like

@rlkoshak Thank you for the feedback!

I am always looking for ways to improve and I greatly appreciate your guidance. I do believe there is a performance hit with the sleep delays but the amp is a bit slow in updating the serial responses. Moving too quickly often results in missing execution or reading of a feedback response from the amp all together. I did see a performance improvement in using the groups design pattern. I reduced the number of lines of code by at least 1/3. I figured there must be a better way with the other functions as well but I am learning this as I go.

I’ll give some of your suggestions a try and report back.

It isn’t a simple performance hit you have to worry about. It is the risk that you completely lock up your ability to run ANY rules, or potentially causing huge delays in the execution of your rules.

Avoiding long sleeps isn’t too improve performance. It is to avoid breaking your oh.

1 Like

I updated my original post and added most of your suggestions. I was able to remove/reduce many of the thread delays.

The items/sitemap file has renumbered items. This not only allows the use of the loop code suggested but also allows users that have more than one Monoprice amp connected together to continue adding zones to their items file with the starting digit “2” for the second unit with no other changes required to the rules.

I appreciate the feedback!

1 Like

This is really good work!

I have just installed two of these amps as master/slave and will be trying your code out.

It would be nice to also see some examples how you have set up the audio sources together with the amps in the UI. What kind of players do you use and how to make a good user interface?

@mrfrenzy
The sitemap code above is the majority of what I have done for an interface. I am using Habpanel for an interface in one of my zones (a bathroom) and I intend on expanding the usage of Habpanel in the remaining zones, as I get time/need.

I currently have all 6 output zones from the amp set up. Currently, I only use 3 inputs/sources. I have a DirectTV STB, and Onkyo and Yamaha receivers. The receivers are completely controllable with currently available OH bindings.

I can control all I have hooked up with the sitemap, although it is very basic.

On my todo list, is setting up the PA functionality of the amp to work with the voice functionality in OH.

@mrfrenzy

Here’s what my sitemap looks like (I haven’t incorporated @swamiller 's code improvements into my setup yet, but you’ll get the idea:

OFF:

ON:

I’ve connected 4 inputs on my amp, but mostly use the Chromecast Audio I have connected to it. I have 4 zones outputting around the house and in my shed.

@swamiller, I’m trying to merge your code changes back into mine (have to use only a single status item for serial connections and remove the hex characters), but I think I’m having a problem getting the mapdb persistence to work (which is making all my commands send to a “null” zone and error out). I haven’t used mapdb persistence until this (still using rrd4j). Can you post your persistence strategy config file? It would be useful for everyone implementing this, I’m sure.

Thanks!

EDIT: Ignore this post, see my response below…

So, after looking over @rlkoshak’s suggestions again, I saw the “triggeringItem” variable he mentioned - what a wonderful, long-overdue addition to the rules engine!!! :slight_smile: I removed the mapdb persistence from your rules, @swamiller, and edited my top post to add the serial-based implementation of your changes.

Thanks again for your hard work improving my original code!

You already have examples code here to consolidate a lot of this code. Note, you can consolidate even more if you were to upgrade to the 2.3 snapshot and use Features for Rules that work with Groups. I’m not recommending that, but be aware there are changes coming in 2.3 that will make rules like this even easier.

rule "Process Update"
when
    Item MonoPrice_Status_G3 received update or 
    Item MonoPrice_Status_G4 received update or 
    Item MonoPrice_Status_G6 received update or
    Item MonoPrice_Status_G7 received update or
    Item MonoPrice_Status_G8 received update or 
    Item MonoPrice_Status_G9 received update  or
    Item MonoPrice_Status_G10 received update
then
    val num = MonoPrice_Status_G1.state as Number

    val type = triggeringItem.name.split("_").get(2)

    switch(type){
        case "G3": postUpdate("MonoPrice_Z"+num+"_Power", if(triggeringItem.state == "01") "ON" else "OFF")
        case "G4": postUpdate("MonoPrice_Z"+num+"_Mute", if(triggeringItem.state == "01") "ON" else "OFF")
        case "G6": {
            val vol = (triggeringItem.state as Number) / 38 * 100
            postUpdate("MonoPrice_Z"+num+"_Volume", vol.toString)
        }
        case "G7": {
            val tre = (triggeringItem.state as Number) / 14 * 100
            postUpdate("MonoPrice_Z"+num+"_Treble", tre.toString)
        }
        case "G8": {
            val bas = (triggeringItem.state as Number) / 14 * 100
            postUpdate("MonoPrice_Z"+num+"_Bass", bas.toString)
        }
        case "G9": {
            val bal = (triggeringItem.state as Numnber) / 20 * 100
            postUpdate("MonoPrice_Z"+num+"_Balance", bal.toString)
        }
        case "G10": {
             postUpdate("MonoPrice_Z"+num+"_Source", triggeringItems.toString)
        }
    }
end

You can collapse your switch statements in your zone rules to

    val znum = triggeringItem.name.split("_").get(1) 
    val zone = znum.substring(string.length() - 1)

For the treble rule you just need to add the line:

val tone_z = zone+"TR"

Even if there were not these String parsing shortcuts, you could use a lambda to consolidate the switch statements

These suggestions should drop your LOC by a third or so. No arrays necessary.

1 Like

@rlkoshak, VERY COOL!! Thanks for the heads up, that Member Of group handler will do wonders for my rules, especially the metric crap-ton of JSON rules in my MQTT LED strip implementation ( NodeMCU MQTT LED Strip Controller Build & Config How-To Videos ). I could already improve that code tenfold with triggeringItem, but I’ll wait until 2.3 is officially out and then do it all in one shot.

BTW, the post you replied to is ~2.5 years old, back when I was just starting my adventure with OH :slight_smile:

1 Like

Somehow I got mixed up.

At least pointing out the group triggers was useful.

This amp is currently on sale. I’m wondering if this is truly a sale or if it’s just been marked down as it’s a few years old; has anyone been keeping track?

For those who own it: are you overall happy? I’m thinking about picking one up for my new house, though I haven’t purchased the home yet. I’m wary to pick up yet another piece of equipment to move, but the sale price is compelling.