Monoprice 6-zone Audio amp items, sitemap & rules

…and here are the rules that didn’t fit in the previous message :wink:

import java.util.regex.Matcher
import java.util.regex.Pattern
import org.apache.commons.lang.StringUtils

var Timer aliveCheckTimeoutTimer = null
val java.util.Map<String, Timer> inputZoneTimers = newHashMap

val updateAmpValue = [ String zoneId, String actionCode, String value, Functions.Function4<String, String, String, Functions.Function1<String, Integer>, Integer> getAmpValueAsPercentage, Functions.Function1<String, Integer> getMaxAmpValue | 
    logDebug("dax66.rules", "func updateAmpValue: On zoneId " + zoneId + ", set ampValue(" + actionCode + ") = " + value)
    switch actionCode {
        case "PA": { postUpdate("Audio_Zones_Dax66Amp_" + zoneId + actionCode, if(value == "01") "ON" else "OFF") }//Public Address
        case "PR": { postUpdate("Audio_Zones_Dax66Amp_" + zoneId + actionCode, if(value == "01") "ON" else "OFF") }//Power
        case "MU": { postUpdate("Audio_Zones_Dax66Amp_" + zoneId + actionCode, if(value == "01") "ON" else "OFF") }//Mute
        case "DT": { postUpdate("Audio_Zones_Dax66Amp_" + zoneId + actionCode, if(value == "01") "ON" else "OFF") }//Do Not Disturb 
        case "VO": { postUpdate("Audio_Zones_Dax66Amp_" + zoneId + actionCode, getAmpValueAsPercentage.apply(zoneId, actionCode, value, getMaxAmpValue).toString) }//Volume
        case "TR": { postUpdate("Audio_Zones_Dax66Amp_" + zoneId + actionCode, getAmpValueAsPercentage.apply(zoneId, actionCode, value, getMaxAmpValue).toString) }//Treble
        case "BS": { postUpdate("Audio_Zones_Dax66Amp_" + zoneId + actionCode, getAmpValueAsPercentage.apply(zoneId, actionCode, value, getMaxAmpValue).toString) }//Bass
        case "BL": { postUpdate("Audio_Zones_Dax66Amp_" + zoneId + actionCode + "R", getAmpValueAsPercentage.apply(zoneId, actionCode, value,getMaxAmpValue).toString) }//Balance
        case "CH": { postUpdate("Audio_Zones_Dax66Amp_" + zoneId + actionCode, value.right(1)) }//Source
        case "LS": { postUpdate("Audio_Zones_Dax66Amp_" + zoneId + actionCode, if(value == "01") "ON" else "OFF") }//Keypad connected
    }
]

val getAmpValueAsPercentage = [ String zoneId, String actionCode, String value, Functions.Function1<String, Integer> getMaxAmpValue | 
    val int maxAmpValue = getMaxAmpValue.apply(actionCode)
    val result = Math::round((Double::parseDouble(value) / maxAmpValue) * 100).intValue
    result
]

val getMaxAmpValue = [ String actionCode | 
    var int maxAmpValue
    switch actionCode {
        case "VO": maxAmpValue = 38
        case "TR": maxAmpValue = 14
        case "BS": maxAmpValue = 14
        case "BL": maxAmpValue = 20
    }
    maxAmpValue
]
rule "Dax66 Sending RS232"
when
    Item Audio_Zones_Dax66Amp_Send received update
then
    var String sendData = Audio_Zones_Dax66Amp_Send.state.toString
    Audio_Zones_Dax66Amp_RS232_Buffer.sendCommand(sendData)

    //This code will take care of race-conditions between frequent user input and zone update requests.
    //The idea is to wait until no user input has been done during last second in a zone before requesting an update from the amp.
    if(sendData.startsWith("<")) {
        val String zoneId = sendData.substring(1, 3)
        if(inputZoneTimers.containsKey(zoneId) && inputZoneTimers.get(zoneId) !== null) {
            inputZoneTimers.get(zoneId).reschedule(now.plusMillis(1000))
        } else {
            inputZoneTimers.put(zoneId, createTimer(now.plusMillis(1000)) [|
                Audio_Zones_Dax66Amp_Send.sendCommand("?" + zoneId + "\r")//Request an update for current zone or amp unit, just to be sure everything is up to date.
                inputZoneTimers.get(zoneId).cancel
                inputZoneTimers.remove(zoneId)
            ])
        }
    }
end

rule "Dax66 Check for received data from RS232 send/receive"
when
    Item Audio_Zones_Dax66Amp_RS232_Buffer received update
then
    //All data (send AND receive) is concatenated.
    //Each data block is terminated with at least one '\r' then a '\n' and then finally a '#' char.
    //Note: Double CR chars, '\r\r\n#' probably means the end of a RECEIVED block.
    var String rawBuffer = Audio_Zones_Dax66Amp_RS232_Buffer.state.toString
    logDebug("dax66.rules", "------>   RS232 Send/Receive: Audio_Zones_Dax66Amp_RS232_Buffer = [" + rawBuffer.replace('\r','\\r').replace('\n','\\n') + "]")
    if(rawBuffer.equals("\r\n#")) {//Master unit is powered on...
        Audio_Zones_Dax66Amp_Refresh.sendCommand(ON)//...start alive check
        return
    }
    var String[] receivedDataBlocks = StringUtils.substringsBetween(rawBuffer, ">", "#")
    if(receivedDataBlocks !== null) {
        for(String receivedData: receivedDataBlocks) {
            Audio_Zones_Dax66Amp_Receive.postUpdate(receivedData.remove("\r").remove("\n"))
            //Need this sleep here or else doubles of same data is posted to Audio_Zones_Dax66Amp_Receive
            Thread::sleep(50)
        }
    }
end

rule "Dax66 Process received data"
when
    Item Audio_Zones_Dax66Amp_Receive received update
then
    val String receivedData = Audio_Zones_Dax66Amp_Receive.state.toString
    if(receivedData !== null) {
        logInfo("dax66.rules", "Process received data: Data = [" + receivedData + "]")

        //The two rows below updates unit alive awareness for the unit that sent the received data.
        val targetSyncItem = gDax66AliveCheck.members.findFirst[ name.equals("Audio_Zones_Dax66Amp_Alive_Check_" + receivedData.left(1) + "0")]
        if(targetSyncItem.state == OFF) { targetSyncItem.sendCommand(ON) }

        var Pattern pattern = null 
        var Matcher matcher = null
        val bigResponseValueCount = 11
        if(receivedData.length == (bigResponseValueCount * 2)) {
            pattern = Pattern::compile("(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)(\\d\\d)")
            matcher = pattern.matcher(receivedData)
            if(matcher.find()) {
                for(var int i = 2; i <= bigResponseValueCount; i=i+1)
                {
                    var String actionCode = "N/A"
                    switch i {
                        case 2: { actionCode = "PA" }//Public Address
                        case 3: { actionCode = "PR" }//Power
                        case 4: { actionCode = "MU" }//Mute
                        case 5: { actionCode = "DT" }//Do Not Disturb 
                        case 6: { actionCode = "VO" }//Volume
                        case 7: { actionCode = "TR" }//Treble
                        case 8: { actionCode = "BS" }//Bass
                        case 9: { actionCode = "BL" }//Balance
                        case 10: { actionCode = "CH" }//Source
                        case 11: { actionCode = "LS" }//Keypad connected
                    }
                    logDebug("dax66.rules", "Process received data: zoneId " + matcher.group(1) + ", ampValue(" + actionCode + ") = " + matcher.group(i))
                    updateAmpValue.apply(matcher.group(1), actionCode, matcher.group(i), getAmpValueAsPercentage, getMaxAmpValue)
                }
            } else {
                logError("dax66.rules", "Process received data: matcher.find() = FAIL")
            }
        } else if(receivedData.length == 6) {
            pattern = Pattern::compile("(\\d\\d)(\\s\\s)(\\d\\d)")
            matcher = pattern.matcher(receivedData)
            if(matcher.find()) {
                logDebug("dax66.rules", "Process received data: zoneId " + matcher.group(1) + ", ampValue(" + matcher.group(2) + ") = " + matcher.group(3))
                updateAmpValue.apply(matcher.group(1), matcher.group(2), matcher.group(3), getAmpValueAsPercentage, getMaxAmpValue)
            }
        } else {
            logError("dax66.rules", "Process received data: FAILED! receivedData.length = " + receivedData.length)
        }
    }
end

rule "Refresh values from Dax66 at alive confirm"
when
    Member of gDax66AliveCheck received command
then
    if(receivedCommand == ON) {
        val aliveUnitId = triggeringItem.name.right(2)
        Audio_Zones_Dax66Amp_Send.sendCommand("?" + aliveUnitId + "\r")
    }
end

//Can't trust openHAB persistence because Dax66 values may have changed during openHAB offline time.
rule "Check if Dax66 is alive at openHAB startup"
when
    Item Audio_Zones_Dax66Amp_Refresh received command or
    System started
then 
    //Reset alive check status
    Audio_Zones_Dax66Amp_Alive_Check_10.postUpdate(OFF)
    Audio_Zones_Dax66Amp_Alive_Check_20.postUpdate(OFF)
    Audio_Zones_Dax66Amp_Alive_Check_30.postUpdate(OFF)

    //Send refresh requests and wait 5 seconds for everything to process
    aliveCheckTimeoutTimer = createTimer(now.plusSeconds(5)) [|
        aliveCheckTimeoutTimer.cancel
        aliveCheckTimeoutTimer = null
        logInfo("dax66.rules", "Alive check: Master = " + Audio_Zones_Dax66Amp_Alive_Check_10.state + "   Slave 1 = " + Audio_Zones_Dax66Amp_Alive_Check_20.state + "   Slave 2 = " + Audio_Zones_Dax66Amp_Alive_Check_30.state)
    ]

    for(var int i = 1; i <= 3; i++) {
        Thread::sleep(1000)
        Audio_Zones_Dax66Amp_Send.sendCommand("?" + i + "1PR\r")
    }
end

rule "Dax66 command handler"
when
    Member of gDax66CommandReceiver received command
then
    if(triggeringItem.state == NULL) { return }
    val String target = triggeringItem.name.substringAfterLast('_')
    val String zoneId = target.substring(0, 2)
    val String actionCode = target.substring(2, 4)
    logDebug("Dax66", "Command handler: triggeringItem.getType = " + triggeringItem.getType + "   zoneId = " + zoneId + "   actionCode = " + actionCode)

    switch triggeringItem.getType {
        case "Switch":  Audio_Zones_Dax66Amp_Send.sendCommand("<" + zoneId + actionCode + if(receivedCommand == ON) "01\r" else "00\r")
        case "Dimmer":  {
            var int maxAmpValue = getMaxAmpValue.apply(actionCode)
            var Number currentValue
            var Number newValue
            logDebug("Dax66", "Dimmer item: triggeringItem.state.getClass = " + triggeringItem.state.getClass.toString)
            if(receivedCommand instanceof PercentType) {           
                currentValue = receivedCommand
                newValue = Math::round(currentValue.doubleValue * (maxAmpValue.doubleValue / 100))             
                logDebug("Dax66", "Dimmer item (PercentType): newValue = " + currentValue + " -> " + newValue)
            } else if(receivedCommand instanceof IncreaseDecreaseType) {
                currentValue = Math::round(Double::parseDouble(triggeringItem.state.toString) * (maxAmpValue.doubleValue / 100))
                if(receivedCommand==INCREASE && currentValue < maxAmpValue) {
                    newValue = currentValue + 1
                    logDebug("Dax66", "Dimmer item (IncreaseDecreaseType/INCREASE): newValue = " + currentValue + " -> " + newValue)
                } else if(receivedCommand==DECREASE && currentValue > 0) {
                    newValue = currentValue - 1
                    logDebug("Dax66", "Dimmer item (IncreaseDecreaseType/DECREASE): newValue = " + currentValue + " -> " + newValue)
                } else { return }
            } else { 
                logWarn("Dax66", "Dimmer item: Unsupported receivedCommand type = " + receivedCommand.getClass.toString)
                triggeringItem.postUpdate(Math::round(((maxAmpValue.doubleValue / 2) / maxAmpValue) * 100) as Number) 
            }

            logDebug("Dax66", "Dimmer item: " + Math::round((currentValue.doubleValue / maxAmpValue) * 100) + "% -> " + Math::round((newValue.doubleValue / maxAmpValue) * 100) + "%")
            triggeringItem.postUpdate(Math::round((newValue.doubleValue / maxAmpValue) * 100) as Number)

            if(actionCode.equals("BL") && target.substring(4, 5).equals("L")) {
                newValue = 20 - newValue// Do some reverse calculation if LEFT balance is the triggering item. 
            }
            Audio_Zones_Dax66Amp_Send.sendCommand("<" + zoneId + actionCode + (if (newValue < 10) "0" else "") + newValue.toString + "\r")
        }
    }
end

rule "Dax66 sync left and right balance dimmers"
when
    Member of gDax66Balance received update
then
    val String targetSyncSide = if(triggeringItem.name.endsWith("L")) "R" else "L"
    val targetSyncItem = gDax66Balance.members.findFirst[ name.equals(triggeringItem.name.left(triggeringItem.name.length - 1) + targetSyncSide)]
    if(targetSyncItem.state == NULL || Integer::parseInt(triggeringItem.state.toString) + Integer::parseInt(targetSyncItem.state.toString) != 100)
        targetSyncItem.postUpdate(100 - Integer::parseInt(triggeringItem.state.toString))
    logDebug("Dax66", "Balance dimmers: " + triggeringItem.name + " = " + triggeringItem.state + "  ==>  " + targetSyncItem.name + " = " + (100 - Integer::parseInt(triggeringItem.state.toString)))
end

rule "Dax66 Zone Source"
when
        Member of gDax66Source received command
then
        val zoneIdAndActionCode = triggeringItem.name.right(4)
        Audio_Zones_Dax66Amp_Send.sendCommand("<" + zoneIdAndActionCode + "0" + receivedCommand.toString + "\r")
end

3 Likes