I’m still learning the OH and Functions and all that. Right now, I’m in the process of converting all my HA control over from other platform. The local processing of the OH provide possibility of more reliably tapping pattern detection of the physical switch. So I wrote a Lambda function that can pretty easily applied to any switch you have. And render them capability to detect double-tap, triple tap, or pretty much whatever pattern you can think of. I hope this effort is not redundant to some other members, as I’m still very new to OH and had no OH1 experience at all.
Usage
First copy this code to the beginning of your rule:
import java.util.ArrayList
import java.util.LinkedList
import java.util.Map
import java.util.regex.Pattern
import org.eclipse.smarthome.core.library.items.SwitchItem
import org.eclipse.smarthome.model.script.actions.Timer
import org.eclipse.xtext.xbase.lib.Functions
import org.eclipse.xtext.xbase.lib.Pair
import org.eclipse.xtext.xbase.lib.Procedures
import org.joda.time.DateTime
/*
* ======================================= Tap Pattern Detection ======================================
* Ver. 1.0
* Author: Victor Bai
* Date: Mar 17, 2017
*
* Description: Function to detect switch's tapping pattern: double-tap, triple pattern, customized pattern, etc.
*/
/*
* Data structure to hold Tap pattern states (don't edit this, or care about it):
* {SWITCH -> [
* [{'ON' => '2017-03-12 12:30:10:125'}, {'ON' => '2017-03-12 12:30:11:125'}], // States Queue: LinkedList<Pair<String, DateTime>>
* [Pattern, Pattern, Pattern] // Holder for Pattern Strings' RegEx compilation: ArrayList<Pattern>
* [2, 0, 1], // Match results. ArrayList<Integer>. Same order as the input pattern. 0: no match; 1: partial match; 2: complete match
* Timer // Timer
* ]
* }
*/
val Map<SwitchItem, LinkedList<?>> tapStates = newHashMap
/*
* Function detectUpdatePatterns.apply(SwitchItem, tapStates, patternConfig)
*
* Input Arguments:
* SwitchItem: Switch triggering the tap pattern(s)
* tapStates: rule scope predefined variable
* patternConfig: ArrayList<Pair<SwitchItem, String>> pattern definition. e.g. newArrayList(V_Office_DoubleTap->'ON,ON', V_Office_TripleTap->'ON,ON,-,ON')
* "SwitchItem" here is the virtual/proxy item you want to switch ON whenever associated pattern is detected
* "String" representing pattern composed of ',' delimited states. Possible state values are:
* "ON": state of ON
* "OFF": state of OFF
* "-": Represent a "skipping" of a tapping. you have to time it right. if it's longer than LONG_SPAN, previous tapping will be discarded
* "#": Represent any DimmerSwitch state in Positive percentage
* "0": Represent any DimmerSwitch's OFF state
* return:
* ArrayList<Integer>: list of int value indicating matching result: 0: no match; 1: partial match; 2: complete match
*
*/
val Functions.Function3<
SwitchItem, // Switch triggering tapping
Map<SwitchItem, LinkedList<?>>, // Global caching variable
ArrayList<Pair<SwitchItem, String>>, // Pattern Configuration
ArrayList<Integer> // return list of int value indicating matching result.
> detectUpdatePatterns = [
SwitchItem light,
Map<SwitchItem, LinkedList<?>> tapStates,
ArrayList<Pair<SwitchItem, String>> patternConfig |
// Pre-configured time span between tapping
val SHORT_SPAN = 800 // 800ms, sequence of key pressing must happened within this time frame
val LONG_SPAN = 2*SHORT_SPAN // If time between tapping extend beyond this time, previous tapping will be ignored
// initialize
if (tapStates.get(light) == null)
tapStates.put(light, newLinkedList)
val switchState = tapStates.get(light) as LinkedList<Object>
if (switchState.size == 0) {
switchState.add(newLinkedList) // States queue
switchState.add(newArrayList) // RegEx Pattern list
switchState.add(newArrayList) // Match results
for (i : 0..<patternConfig.size)
(switchState.get(2) as ArrayList<Integer>).add(-1)
switchState.add(null) // timer
// generate patternString
// var String[] patternStrings = newArrayList
for (var i = 0; i < patternConfig.size; i++) {
var pattern = Pattern.compile('^' + patternConfig.get(i).value.replaceAll('\\s', '').replaceAll('#', '[1-9]\\d*'))
(switchState.get(1) as ArrayList<Pattern>).add(pattern)
logInfo(light.name, "Pattern[{}]: '{}' => '{}'", i, patternConfig.get(i).value, pattern)
}
logInfo(light.name, "detectUpdatePatterns initialized.")
}
val stateQueue = switchState.get(0) as LinkedList<Pair<String, DateTime>>
val patterns = switchState.get(1) as ArrayList<Pattern>
val matchResult = switchState.get(2) as ArrayList<Integer>
var tapTimer = switchState.get(3) as Timer
var eventTime = new DateTime
// pushout the old state from the queue
if (stateQueue.size > 0 && eventTime.isAfter(stateQueue.last.value.plusMillis(LONG_SPAN))) {
//logInfo(light.name, "Pushout the old state from the queue.")
stateQueue.clear
}
stateQueue.add(light.state.toString -> eventTime)
//logInfo(light.name, "stateQueue.size = {}", stateQueue.size)
//compose comparing string from existing queue
var queueString = ""
var DateTime last = null
for (var i = 0; i < stateQueue.length; i++) {
var state = stateQueue.get(i)
var stateTime = state.value
if (last != null && stateTime.isAfter(last.plusMillis(SHORT_SPAN))) {
queueString += ",-"
}
if (i > 0) queueString += ","
queueString += state.key
last = state.value
}
//logInfo(light.name, "queueString = {}", queueString)
// matching against patterns
var partialMatch = -1
var completeMatch = -1
for (i: 0..< patterns.size) {
//logInfo(light.name, "Pattern {} = '{}'", light.name, patterns.get(i).toString)
var matcher = patterns.get(i).matcher(queueString)
if (matcher.matches) {
completeMatch = i
matchResult.set(i, 2)
}
else if (matcher.hitEnd) {
// Partial match found
matchResult.set(i, 1)
partialMatch = i
}
else
matchResult.set(i, 0)
//logInfo(light.name, "matchResult[{}] = {}", i, matchResult.get(i))
}
val comp = completeMatch
if (completeMatch > -1) {
logInfo(light.name, "Complete match found for pattern: {}.", patternConfig.get(comp).value)
tapTimer?.cancel
tapTimer = null
if (partialMatch > -1) {
logInfo(light.name, "Partial match found for pattern: {}", patternConfig.get(partialMatch).value)
if (patternConfig.get(comp).key != null) {
// schedule timer for matched pattern
logInfo(light.name, "Delaying triggering for complete matched pattern: {}", patternConfig.get(comp).value)
tapTimer?.cancel
tapTimer = createTimer(now.plusMillis(LONG_SPAN))[|
stateQueue.clear
logInfo(light.name, "Fire scheduled pattern '{}' event to Switch({})", patternConfig.get(comp).value, patternConfig.get(comp).key.name)
patternConfig.get(comp).key.sendCommand(ON)
]
switchState.set(3, tapTimer)
}
else {
// No switch to receive the complete match event while partial match exist
// Do nothing, up to caller to decide the action path based on matchResult
}
}
else {
stateQueue.clear
if (patternConfig.get(comp).key != null)
patternConfig.get(comp).key.sendCommand(ON)
}
}
else if (partialMatch > -1) {
// Partial match is in progress, we should stop previous delayed complete match
tapTimer?.cancel
}
return matchResult
]
Next think of your patterns. Patterns and examples are briefly documented in the above comments and is defined in String. “state,state,…state” delimited by comma. Each state can have below values:
“ON”: state of ON
"OFF": state of OFF
"-": Represent a “skipping” of a tapping. you have to time it right. if it’s longer than LONG_SPAN, previous tapping will be discarded
"#": Represent any DimmerSwitch state in Positive percentage
"0": Represent any DimmerSwitch’s OFF state
Then the argument passed to the function is of type ArrayList<Pair<SwitchItem, String>>. As you can see, it’s a list (representing list of patterns). Each pattern has a paired SwitchItem serve as event receiving proxy items (It can be null).
Multiple Patterns priority rules: something to consider when designing your complicated multiple patterns.
- In case of overlapping patterns. Longer pattern will have higher priority over shorter pattern. e.g. “ON,ON,ON” > “ON,ON”. It also means if shorter pattern get a complete match, while longer pattern get a partial match, it’ll wait until later before firing.
- If overlapping patterns received a tapping sequence that partial match detected, also with previous complete match waiting to be fired. Then previous complete match will be discarded in favour of longer pattern detection. e.g. “ON,ON” and “ON,ON,OFF,ON”. When tap sequence of “ON->ON->OFF” detected, no double tap event will be fired later, because it’s discarded assuming the longer pattern is being tried.
- The list is ordered. In case multiple patterns get exact match as same time, the latter element has higher priority over front element. e.g. For patternConfig of ArrayList(switch->“ON,ON”, switch2->“ON,OFF,ON,ON”). Tapping sequence of “ON,OFF,ON,ON” will match both “ON,ON” and “ON,OFF,ON,ON”. But only switch2 will be turned ON.
NOTE: Overlapping pattern could possibly delay detection triggering by some time (default LONG_SPAN=1.6s). (e.g. double tap could means a triple tap is in process, thus wait until the third tap come in and confirm whether it’s a triple tap pattern. If it’s not, then previously matched double tap will fire the trigger)
Invoking the function is relatively easy, and non-intrusive to your existing code (I don’t like writing a lot of repeatative code in the rule definition). You can invoke the lambda function in two ways:
1. Use proxy items to receive detection. Define proxy items to receive each pattern’s detection event. This is preferred if you have overlapping patterns. e.g. In case of “ON,ON”(double tap), and “ON,ON,ON”(triple tap). Double tap will turn on its proxy item briefly later by a Timer if Triple tap was not eventually a match. Here I’m giving an example of what I did for my Switches.
.item file
Switch V_Office_DoubleTap {autoupdate="false"}
Switch V_Office_TripleTap {autoupdate="false"}
.rule file (same rule defining detectUpdatePatterns function)
rule "Testing Office light pattern"
when
Item ZW_Office_Light received update
then
detectUpdatePatterns.apply(ZW_Office_Light, tapStates, newArrayList(V_Office_DoubleTap->'ON,ON', V_Office_TripleTap->'ON,ON,ON'))
end
NOTE: If you want instant pattern detection without delay, you should design your tapping pattern in a way that no overlapping can happen. (e.g. “ON,ON” also partially matches “ON,ON,ON”, but not “ON,-,ON”)
2. Invoke function directly. If you don’t like proxy items. you can just invoke the function, and check the return value to decide what you want to do with detection result, which is a list of int value at same order as your pattern list passed in. Type: ArrayList. e.g. [2,0]. (0: no match; 1: partial match; 2: complete match )
Here is an example I use function without proxy items:
rule "Testing Office light pattern 2"
when
Item ZW_Office_Light received update
then
var matchResult = detectUpdatePatterns.apply(ZW_Office_Light, tapStates, newArrayList(null->'ON,ON', null->'ON,OFF,-,ON'))
if (matchResult.get(0) == 2) {
// Double tap detected, do something
}
else if (matchResult.get(1) == 2) {
// Fancy tap detected, do something
}
end
Configuration / Customization
Within the function, there are two variables to change, you can change them to fine tune the tapping sensitivity and time frame works for you.
val SHORT_SPAN = 800 // 800ms, sequence of key pressing must happened within this time frame
val LONG_SPAN = 2*SHORT_SPAN // If time between tapping extend beyond this time, previous tapping will be ignored
Limitations
- In real life there has to be a non-trivia delay between the tapping (for me this is about 500ms) due to many factors. How much delay should be configured depends on your switch’s responsiveness, your OH server’s performance etc. For me, my GE Z-Wave switch just decide not to send ZWAVE signal out if I tap it too quickly. Or if I press them longer than normal. So I have to practice my timing for a while before I can get to a decent success rate. See, don’t get too excited, this solution is still some distant from “real” physical solution.
- You cannot have more than one “skip” next to each other in your pattern definition. e.g. “ON,-,-,ON”. this is NOT allowed. Since wait too long will cause all previous tapping discarded.
- There are limitations of OH framework: e.g. there is no event properties available. So the timing of each tapping is not really accurate, and could be some delay after the physical signal sent to the hub. I wish there are some properties in the event available in future: e.g. event.isPhysical(); event.createdTime.
- There seems to be limitation of Z-wave binding I’m using: I don’t know which event I should capture for the physical tapping. Right now I’m using “received update”. But by looking the log, many things can trigger this update event: binding itself do the status polling, and if its frequency is high enough (I had add-on switches which doesn’t send physical signal, so I’m relying on frequent polling to get status update asap), it will interfere with your tapping pattern. Also, I haven’t tested this on the dimmer switches, although it theoretically should work. I found whenever I tap on my GE dimmer switch, there’re many update event fired in the log, so it could messed up your tapping sequence. In order for this whole thing work, I need to figure out a way to detect physical event (and more preferably, the accurate time the physical event fired).
Applications
I’m currently applying this to a few switches. One of them on the door entry porch light switch: double tap On will trigger ground floor lights on. double tap OFF to turn off all ground floor lights; triple tap OFF to turn off all lights in the house, and arm the alarm system. You can come up with some interesting tap patterns to do some complicated things, up to your imagination.
Have Fun Tapping!
[Edit: Fixed bugs on 11:30PM PST Mar 17, 2017]