Last active
June 16, 2023 21:12
-
-
Save barbudor/a218e9c9afe8e50d67414ce2ddd0efa2 to your computer and use it in GitHub Desktop.
tasmota.add_rule() to support map argument
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Rule_Matcher2 | |
# We don't actually need a superclass, just implementing `match(val)` | |
# | |
# static class Rule_Matcher | |
# def init() | |
# end | |
# # return the next index in tha val string | |
# # or `nil` if no match | |
# def match(val) | |
# return nil | |
# end | |
# end | |
# Each matcher is an instance that implements `match(val) -> any or nil` | |
# | |
# The method takes a map or value as input, applies the matcher and | |
# returns a new map or value, or `nil` if the matcher did not match anything. | |
# | |
# Example: | |
# Payload#?#Temperature>50 | |
# is decomposed as: | |
# <instance match="Payload">, <instance match_any>, <instance match="Temperature", <instance op='>' val='50'> | |
# | |
# Instance types: | |
# Rule_Matcher_Key(key): checks that the input map contains the key (case insensitive) and | |
# returns the sub-value or `nil` if the key does not exist | |
# | |
# Rule_Matcher_Wildcard: maps any key, which yields to unpredictable behavior if the map | |
# has multiple keys (gets the first key returned by the iterator) | |
# | |
# Rule_Matcher_Operator: checks is a simple value (numerical or string) matches the operator and the value | |
# returns the value unchanged if match, or `nil` if no match | |
static class Rule_Matcher_Key | |
var name # literal name of what to match | |
def init(name) | |
self.name = name | |
end | |
# find a key in map, case insensitive, return actual key or nil if not found | |
static def find_key_i(m, keyi) | |
import string | |
var keyu = string.toupper(keyi) | |
if isinstance(m, map) | |
for k:m.keys() | |
if string.toupper(k)==keyu | |
return k | |
end | |
end | |
end | |
end | |
def match(val) | |
if val == nil return nil end # safeguard | |
if !isinstance(val, map) return nil end # literal name can only match a map key | |
var k = self.find_key_i(val, self.name) | |
if k == nil return nil end # no key with value self.name | |
return val[k] | |
end | |
def tostring() | |
return "<Matcher key='" + str(self.name) + "'>" | |
end | |
end | |
static class Rule_Matcher_Array | |
var index # index in the array, defaults to zero | |
def init(index) | |
self.index = index | |
end | |
def match(val) | |
if val == nil return nil end # safeguard | |
if !isinstance(val, list) return val end # ignore index if not a list | |
if self.index <= 0 return nil end # out of bounds | |
if self.index > size(val) return nil end # out of bounds | |
return val[self.index - 1] | |
end | |
def tostring() | |
return "<Matcher [" + str(self.index) + "]>" | |
end | |
end | |
static class Rule_Matcher_Wildcard | |
def match(val) | |
if val == nil return nil end # safeguard | |
if !isinstance(val, map) return nil end # literal name can only match a map key | |
if size(val) == 0 return nil end | |
return val.iter()() # get first value from iterator | |
end | |
def tostring() | |
return "<Matcher any>" | |
end | |
end | |
static class Rule_Matcher_Map | |
var trigger_map # a map object to match in event map | |
def init(trigger_map) | |
self.trigger_map = trigger_map | |
end | |
def map_match(sub_map, in_map) | |
for k: sub_map.keys() | |
if in_map.find(k) == nil | |
return false | |
elif isinstance(sub_map[k], map) | |
if !self.map_match(sub_map[k], in_map[k]) | |
return false | |
end | |
elif sub_map[k] != in_map[k] | |
return false | |
end | |
end | |
return true | |
end | |
def match(event_map) | |
if event_map == nil return nil end # safeguard | |
if !isinstance(event_map, map) return nil end # sub_map can only match a map | |
if !self.map_match(self.trigger_map, event_map) return nil end | |
return true | |
end | |
def tostring() | |
return "<Matcher obj=" + str(self.obj) + ">" | |
end | |
end | |
static class Rule_Matcher_Operator | |
var op_func # function making the comparison | |
var op_str # name of the operator like '>' | |
var op_value # value to compare agains | |
def init(op_str, op_value) | |
self.op_parse(op_str, op_value) | |
end | |
########################################################################################### | |
# Functions to compare two values | |
########################################################################################### | |
def op_parse(op, op_value) | |
self.op_str = op | |
def op_eq_str(a,b) return tasmota._apply_str_op(1, str(a), b) end | |
def op_neq_str(a,b) return tasmota._apply_str_op(2, str(a), b) end | |
def op_start_str(a,b) return tasmota._apply_str_op(3, str(a), b) end | |
def op_end_str(a,b) return tasmota._apply_str_op(4, str(a), b) end | |
def op_sub_str(a,b) return tasmota._apply_str_op(5, str(a), b) end | |
def op_notsub_str(a,b) return tasmota._apply_str_op(6, str(a), b) end | |
def op_eq(a,b) return number(a) == b end | |
def op_neq(a,b) return number(a) != b end | |
def op_gt(a,b) return number(a) > b end | |
def op_gte(a,b) return number(a) >= b end | |
def op_lt(a,b) return number(a) < b end | |
def op_lte(a,b) return number(a) <= b end | |
def op_mod(a,b) return (int(a) % b) == 0 end | |
var numerical = false | |
var f | |
if op=='=' f = op_eq_str | |
elif op=='!==' f = op_neq_str | |
elif op=='$!' f = op_neq_str | |
elif op=='$<' f = op_start_str | |
elif op=='$>' f = op_end_str | |
elif op=='$|' f = op_sub_str | |
elif op=='$^' f = op_notsub_str | |
else | |
numerical = true | |
if op=='==' f = op_eq | |
elif op=='!=' f = op_neq | |
elif op=='>' f = op_gt | |
elif op=='>=' f = op_gte | |
elif op=='<' f = op_lt | |
elif op=='<=' f = op_lte | |
elif op=='|' f = op_mod | |
end | |
end | |
self.op_func = f | |
if numerical # if numerical comparator, make sure that the value passed is a number | |
# to check if a number is correct, the safest method is to use a json decoder | |
import json | |
var val_num = json.load(op_value) | |
if type(val_num) != 'int' && type(val_num) != 'real' | |
raise "value_error", "value needs to be a number" | |
else | |
self.op_value = val_num | |
end | |
else | |
self.op_value = str(op_value) | |
end | |
end | |
def match(val) | |
var t = type(val) | |
if t != 'int' && t != 'real' && t != 'string' return nil end # must be a simple type | |
return self.op_func(val, self.op_value) ? val : nil | |
end | |
def tostring() | |
if type(self.op_value) == 'string' | |
return "<Matcher op '" + self.op_str + "' val='" + str(self.op_value) + "'>" | |
else | |
return "<Matcher op '" + self.op_str + "' val=" + str(self.op_value) + ">" | |
end | |
end | |
end | |
########################################################################################### | |
# instance variables | |
var rule # original pattern of the rules | |
var trigger # rule pattern of trigger, excluding operator check (ex: "AA#BB>50" would be "AA#BB") | |
var matchers # array of Rule_Matcher(s) | |
def init(rule, trigger, matchers) | |
self.rule = rule | |
self.trigger = trigger | |
self.matchers = matchers | |
end | |
# parses a rule pattern and creates a list of Rule_Matcher(s) | |
static def parse(pattern) | |
import string | |
if pattern == nil return nil end | |
var matchers = [] | |
if isinstance(pattern, map) | |
matchers.push(_class.Rule_Matcher_Map(pattern)) | |
return _class(pattern, "", matchers) # `_class` is a reference to the Rule_Matcher class | |
end | |
print("not a map") | |
# changes "Dimmer>50" to ['Dimmer', '>', '50'] | |
# Ex: DS18B20#Temperature<20 | |
var op_list = tasmota.find_op(pattern) | |
# ex: 'DS18B20#Temperature' | |
var value_str = op_list[0] | |
var op_str = op_list[1] | |
var op_value = op_list[2] | |
var sz = size(value_str) | |
var idx_start = 0 # index of current cursor | |
var idx_end = -1 # end of current item | |
while idx_start < sz | |
# split by '#' | |
var idx_sep = string.find(value_str, '#', idx_start) | |
var item_str | |
if idx_sep >= 0 | |
if idx_sep == idx_start raise "pattern_error", "empty pattern not allowed" end | |
item_str = value_str[idx_start .. idx_sep - 1] | |
idx_start = idx_sep + 1 | |
else | |
item_str = value_str[idx_start .. ] | |
idx_start = sz # will end the loop | |
end | |
# check if there is an array accessor | |
var arr_start = string.find(item_str, '[') | |
var arr_index = nil | |
if arr_start >= 0 # we have an array index | |
var arr_end = string.find(item_str, ']', arr_start) | |
if arr_end < 0 raise "value_error", "missing ']' in rule pattern" end | |
var arr_str = item_str[arr_start + 1 .. arr_end - 1] | |
item_str = item_str[0 .. arr_start - 1] # truncate | |
arr_index = int(arr_str) | |
end | |
if item_str == '?' | |
matchers.push(_class.Rule_Matcher_Wildcard()) | |
else | |
matchers.push(_class.Rule_Matcher_Key(item_str)) | |
end | |
if arr_index != nil | |
matchers.push(_class.Rule_Matcher_Array(arr_index)) | |
end | |
end | |
# if an operator was found, add the operator matcher | |
if op_str != nil && op_value != nil # we have an operator | |
matchers.push(_class.Rule_Matcher_Operator(op_str, op_value)) | |
end | |
return _class(pattern, value_str, matchers) # `_class` is a reference to the Rule_Matcher class | |
end | |
# apply all matchers, abort if any returns `nil` | |
def match(val_in) | |
if self.matchers == nil return nil end | |
var val = val_in | |
var idx = 0 | |
while idx < size(self.matchers) | |
val = self.matchers[idx].match(val) | |
if val == nil return nil end | |
idx += 1 | |
end | |
return val | |
end | |
def tostring() | |
return str(self.matchers) | |
end | |
end | |
class Tasmota2 : Tasmota | |
static var Rule_Matcher = Rule_Matcher2 | |
end | |
tasmota = Tasmota2() | |
def my_rule_func(v,t,m) | |
print("v:",v) | |
print("t:",t) | |
print("m:",m) | |
end | |
# tasmota.add_rule({"Tele":{"Switch1":"OFF"}}, my_rule_func) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment