Add fancy tap patterns (double tap/triple tap/more) feature to your existing switches

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.

  1. 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.
  2. 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.
  3. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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]

14 Likes

Did you try this with OH1 or OH2?

Is this working with OH2?

Yeah this works with OH2. And works great with WallMote Quads. Only tweak was to use a double tap pattern like ‘1.0,1.0’ instead of ‘ON,ON’

I tried the type of double tap you mention, just to see if I could get two ‘ON’ messages in row, receiving the following from the zwave log;

2018-02-03 20:51:14.777 [DEBUG] [ding.zwave.handler.ZWaveThingHandler] - NODE 14: Got an event from Z-Wave network: ZWaveCommandClassValueEvent
2018-02-03 20:51:14.777 [DEBUG] [ding.zwave.handler.ZWaveThingHandler] - NODE 14: Got a value event from Z-Wave network, endpoint = 0, command class = SWITCH_BINARY, value = 255
2018-02-03 20:51:14.777 [DEBUG] [ding.zwave.handler.ZWaveThingHandler] - NODE 14: Updating channel state zwave:device:7efa0603:node14:switch_binary to ON [OnOffType]

2018-02-03 20:51:15.638 [DEBUG] [ding.zwave.handler.ZWaveThingHandler] - NODE 14: Got an event from Z-Wave network: ZWaveCommandClassValueEvent
2018-02-03 20:51:15.638 [DEBUG] [ding.zwave.handler.ZWaveThingHandler] - NODE 14: Got a value event from Z-Wave network, endpoint = 0, command class = SWITCH_BINARY, value = 255
2018-02-03 20:51:15.638 [DEBUG] [ding.zwave.handler.ZWaveThingHandler] - NODE 14: Updating channel state zwave:device:7efa0603:node14:switch_binary to ON [OnOffType]

The event.log only had the following;
2018-02-03 20:51:14.779 [vent.ItemStateChangedEvent] - zwave_device_7efa0603_node14_switch_binary changed from OFF to ON
2018-02-03 20:51:17.818 [vent.ItemStateChangedEvent] - zwave_device_7efa0603_node14_switch_binary changed from ON to OFF

Is my switch reporting the information that will enable the double-tap approach you describe above?

I know this is an old post but I was wondering if anyone (still) uses this function.

As described, this function would be a nice solution to an automation task I’m working on. However, it appears to be broken in several ways with the latest stable version of OpenHAB (2.4) running in OpenHABian.

I’m still learning the rules language for OpenHAB so I haven’t fully wrapped my head around all the details of the detectUpdatePatterns function. I was able to work through most of the issues I encountered but it still doesn’t work reliably and regularly generates error messages in the log (perhaps as a side effect of some of my fixes but that isn’t entirely clear)

I can go into the details of the problems that I’m seeing if someone is really interested but my question is more about whether anyone has had success running this reliably in the latest stable version of OpenHAB, and if so, what changes did you make to address the issues you encountered.

@mythbai, It looks like you put a lot of work into this so I’m hoping you’re is still around to chime in with any updates.

Worst case, I’ll just (re)write a simpler version for my specific needs but it would be nice to use this if it can be made to work reliably.

I haven’t read through the whole OP, but I use persistence to do something similar. I have music start playing when entering a bathroom, but wanted an easy way to change the station, but Alexa can’t hear over the water noise in the shower. So I installed a contact sensor on the shower door, and can change the station by opening and closing the shower door quickly. If you don’t need all the fancy stuff, you may be able to adapt this to work for you.

if (US_MasterBathroom_Shower_Contact.state == CLOSED && US_MasterBathroom_Shower_Contact.historicState(now.minusSeconds(3)).state == CLOSED && US_MasterBathroom_Shower_Contact.changedSince(now.minusSeconds(3))) {
1 Like

Thanks for sharing your solution. I like the idea of using persistence to help solve this. I think I have something working now using this approach.

For anyone else looking to do something similar as well, here are some notes that might be helpful.
In my case, I wanted to look for an ON, ON tap sequence within 1 second while the light was already on (or OFF, OFF while already OFF). Looking for an ON, OFF, ON, (or CLOSED, OPEN, CLOSED) sequence is easier since those changes are unambiguous and can use the “when item received change” trigger.

There are a flurry of updates that report the same state when a switch changes from ON to OFF or OFF to ON. When idle, OpenHAB also polls the switch on its own every now and then. Since manual switch presses that don’t change state only trigger “received update” (not received commands or changes), the polling events and manual presses look the same. Looking for two updates (same state) in a row within one second, that occur at least 1 second after the last state change seems to be working fairly well.

The whole point of this is to have a convenient way to overwrite my motion sensors if I want the lights to stay on or off regardless of the state of motion in the room.

Thank you for your code!

I use OH3 and get always an error, that null is in the rules:

2021-03-29 21:01:26.840 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID ‘a_pattern_switch-1’ failed: null in a_pattern_switch

What can cause the problem?

Thanks!

Did you make all those OH1-specific imports? Some of those are certainly unwanted in OH3.