DSL is unsophisticated, does have limitations, there are relatively recent features like newState that may have flaws. Regard it as a legacy rule system for OH3 … you may be wasting your time here.
It’s also quite cumbersome (for example in the code I pasted below, my function can’t call the invokeProcedureForItem without an obscure null error I haven’t figured out, so I inlined it in the main function), so I am okay not using it .
So what can I look at? I was looking at JSR 223 and Jython as a way to update my Python a bit, but that’s an old Python (2.7). There’s HAPapp but that runs externally and has some limitations I know (that I don’t understand, being new, so I don’t know if I’d run into them as an issue). JSR 223 with JavaScript? something else like the blocky UI (if it’s there in 3.0)?
That’s the kind of code I am using (inspired from this post), for example. I consider that trivial code (and I would make it a bit better than what I’ve done, data-structures wise, if I wanted to invest in it) and the kind of stuff I expect to write at times. What would be the best other mean to code this, if you were to do so?
import java.time.Duration
import java.time.Instant
import java.util.HashMap
import java.util.LinkedHashMap
import java.util.LinkedList
import java.util.Map
import java.util.regex.Pattern
import org.openhab.core.types.State
/*
* ======================================= Update Pattern Detection ======================================
* Ver. 1.01
* Author: Victor Bai
* Date: Mar 17, 2017
* Updated by Frenchman on Oct 26, 2020
* Description: Function to match 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, Instant>>
* { String -> Pattern, String -> Pattern, String -> Pattern } // Update pattern strings and compiled regular expressions
* [2, 0, 1], // Match results. HashMap<String, Integer>. Same keys as the input pattern. 0: no match; 1: partial match; 2: complete match
* Timer // Timer
* ]
* }
*/
val Map<SwitchItem, LinkedList<?>> states = newHashMap
/*
* Function updatePatternsRecognizer.apply(states, SwitchItem, State, patternConfig)
*
* Input Arguments:
* states: Rule scope predefined variable
* item: SwitchItem switch triggering the tap pattern(s)
* newState: State new state of the switch
* patternConfig: Map<String,Procedures.Procedure0> pattern definition. e.g. newHashMap('ON,ON' -> lambda1, 'ON,ON,-,ON' -> lambda2)
* "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
* "SwitchItem" here is the virtual/proxy item you want to switch ON whenever associated pattern is detected
* Return:
* ArrayList<Integer>: list of int value indicating matching result: 0: no match; 1: partial match; 2: complete match
*
*/
val Functions.Function4<
Map<SwitchItem, LinkedList<?>>, // Global caching variable
SwitchItem, // SwitchItem triggering tapping
State, // New state for the SwitchItem triggering tapping
Map<String, Procedures.Procedure0>, // Patterns configuration
Map<String, Integer> // return list of int value indicating matching result.
> updatePatternsRecognizer = [
Map<SwitchItem, LinkedList<?>> states,
SwitchItem item,
State newState,
Map<String, Procedures.Procedure0> patternsConfig |
val log = "UpdatePatternsRecognizer";
// Pre-configured time span between tapping
val SHORT_SPAN = 400 // Sequence of key pressing must happened within this time frame
val LONG_SPAN = 600 // If time between tapping extend beyond this time, previous tapping will be ignored
// Initialize.
if (states.get(item) === null) {
//logInfo(log, "Creating state for updates of Item '{}'", item.name)
states.put(item, newLinkedList)
}
val itemState = states.get(item) as LinkedList<Object>
if (itemState.size == 0) {
itemState.add(newLinkedList) // States queue.
itemState.add(newLinkedHashMap) // Update patterns and their compiled regular expressions.
itemState.add(newHashMap) // Match results.
for (key : patternsConfig.keySet) {
(itemState.get(2) as HashMap<String, Integer>).put(key, -1)
}
itemState.add(null) // Timer.
// Generate patterns from the strings.
var i = 0
for (key : patternsConfig.keySet) {
var pattern = Pattern.compile('^' + key.replaceAll('\\s', '').replaceAll('#', '[1-9]\\d*'))
(itemState.get(1) as LinkedHashMap<String, Pattern>).put(key, pattern)
//logInfo(log, "Pattern{} for updates of Item '{}' set to '{}' from '{}'", i + 1, item.name, pattern, key)
i = i + 1
}
}
val stateQueue = itemState.get(0) as LinkedList<Pair<String, Instant>>
val updatePatterns = itemState.get(1) as LinkedHashMap<String, Pattern>
val matchResults = itemState.get(2) as HashMap<String, Integer>
var tapTimer = itemState.get(3) as Timer
// Push out the old state from the queue
var eventTime = now.toInstant()
if (stateQueue.size > 0) {
if (eventTime.isAfter(stateQueue.last.value.plusMillis(LONG_SPAN))) {
//logInfo(log, "Resetting updates due to inactivity")
stateQueue.clear
}
}
if (stateQueue.isEmpty()) {
//logInfo(log, "Beginning recognition of updates for Item '{}'", item.name)
}
stateQueue.add(newState.toString -> eventTime)
// Compose comparison string from existing queue.
var queueString = ""
for (var i = 0; i < stateQueue.length; i++) {
var state = stateQueue.get(i)
var stateTime = state.value
if (i > 0) {
queueString += ","
if (stateTime.isAfter(stateQueue.get(i - 1).value.plusMillis(SHORT_SPAN))) {
queueString + "-,"
}
}
queueString += state.key
}
var last = if (stateQueue.length == 0) null else stateQueue.last.value
var first = if (stateQueue.length == 0) null else stateQueue.get(0).value
var duration = if (first === null) 0 else Duration.between(first, last).toMillis()
if (duration != 0) {
//logInfo(log, "Item '{}' updated to {} over {}ms", item.name, queueString, duration)
} else {
//logInfo(log, "Item '{}' updated to {}", item.name, queueString)
}
// Match against regular expressions.
var partialMatch = null
var completeMatch = null
for (entry : updatePatterns.entrySet) {
var matcher = entry.value.matcher(queueString)
if (matcher.matches) {
matchResults.put(entry.key, 2)
completeMatch = entry.key
} else if (matcher.hitEnd) {
// Partial match found
matchResults.put(entry.key, 1)
partialMatch = entry.key
} else {
matchResults.put(entry.key, 0)
}
//logInfo(log, "Updates {} match {}/{} of {} updates", queueString, matchResults.get(entry.key), entry.key.split(",", -1).length, entry.key)
}
if (completeMatch !== null) {
val comp = completeMatch
tapTimer?.cancel
tapTimer = null
if (partialMatch !== null) {
if (patternsConfig.get(comp) !== null) {
// Schedule timer for matched pattern
//logInfo(log, "Waiting to see if updates {} will match {}", patternsConfig.get(comp).key, patternsConfig.get(partialMatch).key)
tapTimer?.cancel
tapTimer = createTimer(now.toInstant().plusMillis(LONG_SPAN)) [|
stateQueue.clear
logInfo(log, "Calling action as Item '{}' updated to {}", item.name, comp)
patternsConfig.get(comp).apply()
//invokeProcedureForItem.apply(item, comp, patternsConfig.get(comp))
]
itemState.set(3, tapTimer)
}
else {
// No action to invoke on complete match, so no need to wait to resolve
// the ambiguity. Let the caller decide what to do basedon matchResults.
}
} else {
stateQueue.clear
if (patternsConfig.get(comp) !== null) {
logInfo(log, "Calling action as Item '{}' updated to {}", item.name, comp)
patternsConfig.get(comp).apply()
//invokeProcedureForItem.apply(item, comp, patternsConfig.get(comp))
}
}
} else if (partialMatch !== null) {
// Partial match is in progress, we should stop previous delayed complete match.
tapTimer?.cancel
}
return matchResults
]
val Procedures.Procedure3<
SwitchItem, // SwitchItem triggering tapping
String, // Pattern
Procedures.Procedure0 // Action
> invokeProcedureForItem = [
SwitchItem item,
String pattern,
Procedures.Procedure0 action |
logInfo(log, "Calling action after Item '{}' updated to {}", item.name, pattern)
action.apply()
]
val Procedures.Procedure2<
SwitchItem, // SwitchItem triggering tapping
Map<SwitchItem, LinkedList<?>> // Global caching variable
> clearUpdates = [
SwitchItem item,
Map<SwitchItem, LinkedList<?>> states |
var state = states.get(item)
if (state !== null) {
logInfo("ClearUpdates", "Clearing updates of Item '{}'", item.name)
state.get(0).clear
}
]
rule "Kitchen scene All Bright"
when
Item KitchenLights_Keypad_Button1 changed to OFF
then
// Id is "Blm0lJ8AEwajtA0" for scene "Bright (Countertops)"
// Id is "49esTNYAU92x2rA" for scene "Concentrate (Countertops)"
updatePatternsRecognizer.apply(states, KitchenLights_Keypad_Button1, newState, newHashMap(
'OFF,OFF' -> [| Countertops_Scene.sendCommand("Blm0lJ8AEwajtA0") ]))
end
rule "Kitchen scene All Off"
when
Item KitchenLights_Keypad_Button3 changed to OFF
then
updatePatternsRecognizer.apply(states, KitchenLights_Keypad_Button3, newState, newHashMap(
'OFF,OFF' -> [| Countertops_Switch.sendCommand(OFF) ]))
end
rule "Kitchen scene All Raise"
when
Item KitchenLights_Keypad_RaiseButton changed to OFF
then
updatePatternsRecognizer.apply(states, KitchenLights_Keypad_RaiseButton, newState, newHashMap(
'OFF,OFF' -> [| Countertops_Brightness.sendCommand(Lightbench_LightLevel.state) ]))
end
rule "Kitchen scene All Lower"
when
Item KitchenLights_Keypad_LowerButton changed to OFF
then
updatePatternsRecognizer.apply(states, KitchenLights_Keypad_LowerButton, newState, newHashMap(
'OFF,OFF' -> [| Countertops_Brightness.sendCommand(Lightbench_LightLevel.state) ]))
end