feat: add support for local expressions in ADExpressionContext and update smart condition handling
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import pyparsing as pp
|
import pyparsing as pp
|
||||||
from expressionparser import BaseExpressionContext
|
from expressionparser import BaseExpressionContext, ExpressionParser
|
||||||
|
|
||||||
class ADExpressionContext(BaseExpressionContext):
|
class ADExpressionContext(BaseExpressionContext):
|
||||||
def __init__(self,appdaemon_api, sensor_existence_validation = True):
|
def __init__(self,appdaemon_api, sensor_existence_validation = True):
|
||||||
@@ -13,6 +13,7 @@ class ADExpressionContext(BaseExpressionContext):
|
|||||||
self.sensors_default_value = dict()
|
self.sensors_default_value = dict()
|
||||||
self.sensor_states = dict()
|
self.sensor_states = dict()
|
||||||
self.sensor_existence_validation = sensor_existence_validation
|
self.sensor_existence_validation = sensor_existence_validation
|
||||||
|
self.local_expressions = {}
|
||||||
|
|
||||||
self.declare_default_constants()
|
self.declare_default_constants()
|
||||||
|
|
||||||
@@ -131,7 +132,22 @@ class ADExpressionContext(BaseExpressionContext):
|
|||||||
def get_entities_to_listen(self):
|
def get_entities_to_listen(self):
|
||||||
return self.entities_to_listen
|
return self.entities_to_listen
|
||||||
|
|
||||||
def get_constant_value(self,name): return self.variables[name]
|
def get_constant_value(self, name):
|
||||||
|
if name in self.local_expressions:
|
||||||
|
return self.local_expressions[name].eval()
|
||||||
|
return self.variables[name]
|
||||||
|
|
||||||
|
def declare_local_expression(self, name: str, expression_string: str):
|
||||||
|
assert name not in self.variables, f"'{name}' is already declared"
|
||||||
|
assert name.replace('_', '').isalnum() and name.isascii(), f"Invalid local expression name: {name}"
|
||||||
|
# Register as a constant placeholder so the variable parser recognises it
|
||||||
|
self.variables[name] = None
|
||||||
|
self.constants.append(name)
|
||||||
|
self.parser = None # reset variable parser cache
|
||||||
|
# Parse the expression; set_context registers its sensor dependencies
|
||||||
|
# Store only the parsed tree so evaluation skips the top-level reset()
|
||||||
|
full_parser = ExpressionParser(expression_string, self)
|
||||||
|
self.local_expressions[name] = full_parser.parsed_data
|
||||||
|
|
||||||
def declare_constant(self,name : str,value):
|
def declare_constant(self,name : str,value):
|
||||||
def is_a_sensor(string):
|
def is_a_sensor(string):
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class Evaluator():
|
|||||||
__LOG_INIT = "log_init"
|
__LOG_INIT = "log_init"
|
||||||
__EXTRA_ENTITIES_TO_LISTEN = "extra_entities_to_listen"
|
__EXTRA_ENTITIES_TO_LISTEN = "extra_entities_to_listen"
|
||||||
__CONSTANTS = "constants"
|
__CONSTANTS = "constants"
|
||||||
|
__LOCAL_EXPRESSIONS_STRING = "local_expressions"
|
||||||
__UNAVAIBILITY_RESULT = "unavaibility_result"
|
__UNAVAIBILITY_RESULT = "unavaibility_result"
|
||||||
|
|
||||||
def __init__(self, appdaemon_api, conditions_block,**kwargs):
|
def __init__(self, appdaemon_api, conditions_block,**kwargs):
|
||||||
@@ -313,6 +314,12 @@ class Evaluator():
|
|||||||
if disable_conditions_args:
|
if disable_conditions_args:
|
||||||
add_conditions_to_conditions_block(conditions_block,self.__DISABLE_CONDITIONS_STRING,disable_conditions_args)
|
add_conditions_to_conditions_block(conditions_block,self.__DISABLE_CONDITIONS_STRING,disable_conditions_args)
|
||||||
|
|
||||||
|
local_expressions_args = param_parser.parse_args(self.__LOCAL_EXPRESSIONS_STRING, None)
|
||||||
|
if local_expressions_args:
|
||||||
|
if self.__LOCAL_EXPRESSIONS_STRING not in conditions_block:
|
||||||
|
conditions_block[self.__LOCAL_EXPRESSIONS_STRING] = {}
|
||||||
|
conditions_block[self.__LOCAL_EXPRESSIONS_STRING].update(local_expressions_args)
|
||||||
|
|
||||||
if param_parser.parse_args(self.__TEMPLATE_CONDITIONS_STRING, None):
|
if param_parser.parse_args(self.__TEMPLATE_CONDITIONS_STRING, None):
|
||||||
self.__log(f"Template '{template}' contain recursive template",level = "ERROR")
|
self.__log(f"Template '{template}' contain recursive template",level = "ERROR")
|
||||||
|
|
||||||
@@ -378,6 +385,18 @@ class Evaluator():
|
|||||||
self.__log(f"{constant_name} = {value}")
|
self.__log(f"{constant_name} = {value}")
|
||||||
self.expression_context.declare_constant(constant_name,value)
|
self.expression_context.declare_constant(constant_name,value)
|
||||||
|
|
||||||
|
local_expressions = param_parser.parse_args(self.__LOCAL_EXPRESSIONS_STRING, None)
|
||||||
|
if local_expressions:
|
||||||
|
if self.__log_init:
|
||||||
|
self.__log(f"declaring local expressions:")
|
||||||
|
for expr_name, expr_string in local_expressions.items():
|
||||||
|
if self.__log_init:
|
||||||
|
self.__log(f"{expr_name} = {expr_string}")
|
||||||
|
self.expression_context.declare_local_expression(expr_name, str(expr_string))
|
||||||
|
# Ensure sensors referenced only in local_expressions are registered as
|
||||||
|
# listeners even if no condition string references them directly.
|
||||||
|
self.__add_to_entities_to_listen(self.expression_context.get_entities_to_listen())
|
||||||
|
|
||||||
|
|
||||||
trigger_conditions_args = param_parser.parse_args(self.__TRIGGER_CONDITIONS_STRING,None)
|
trigger_conditions_args = param_parser.parse_args(self.__TRIGGER_CONDITIONS_STRING,None)
|
||||||
if trigger_conditions_args:
|
if trigger_conditions_args:
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ templates_library:
|
|||||||
trigger_conditions: binary_sensor.off == False
|
trigger_conditions: binary_sensor.off == False
|
||||||
blocking_conditions:
|
blocking_conditions:
|
||||||
- binary_sensor.on
|
- binary_sensor.on
|
||||||
|
T4:
|
||||||
|
local_expressions:
|
||||||
|
local_threshold: "binary_sensor.true ? 170 : 200"
|
||||||
|
trigger_conditions: sensor.int170 == local_threshold
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
test_1:
|
test_1:
|
||||||
@@ -44,3 +48,25 @@ tests:
|
|||||||
conditions:
|
conditions:
|
||||||
template_conditions: <T2>,<T3>
|
template_conditions: <T2>,<T3>
|
||||||
trigger_conditions: binary_sensor.true and binary_sensor.false
|
trigger_conditions: binary_sensor.true and binary_sensor.false
|
||||||
|
|
||||||
|
# local_expressions in a template
|
||||||
|
test_5_local_expr_in_template_true:
|
||||||
|
expected_result: Succeeded
|
||||||
|
conditions:
|
||||||
|
template_conditions: <T4>
|
||||||
|
|
||||||
|
# local_expressions declared directly in conditions block
|
||||||
|
test_6_local_expr_inline_true:
|
||||||
|
expected_result: Succeeded
|
||||||
|
conditions:
|
||||||
|
local_expressions:
|
||||||
|
my_threshold: "binary_sensor.true ? 17 : 200"
|
||||||
|
trigger_conditions: sensor.int17 == my_threshold
|
||||||
|
|
||||||
|
# local_expressions inline, condition not met
|
||||||
|
test_7_local_expr_inline_false:
|
||||||
|
expected_result: Failed
|
||||||
|
conditions:
|
||||||
|
local_expressions:
|
||||||
|
my_threshold: "binary_sensor.false ? 17 : 200"
|
||||||
|
trigger_conditions: sensor.int17 == my_threshold
|
||||||
|
|||||||
Reference in New Issue
Block a user