…and here are the rules that didn’t fit in the previous message
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