From 56e3f6e9e0d6d19971981608566852cd73244a7b Mon Sep 17 00:00:00 2001 From: Pierre Gironde Date: Wed, 3 Jun 2026 21:52:58 +0200 Subject: [PATCH] feat: add support for local expressions in ADExpressionContext and update smart condition handling --- adexpressioncontext.py | 20 ++++++++++++++++++-- smartcondition.py | 21 ++++++++++++++++++++- unit_tests/test_template.yaml | 26 ++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/adexpressioncontext.py b/adexpressioncontext.py index 653757c..c160c26 100644 --- a/adexpressioncontext.py +++ b/adexpressioncontext.py @@ -1,5 +1,5 @@ import pyparsing as pp -from expressionparser import BaseExpressionContext +from expressionparser import BaseExpressionContext, ExpressionParser class ADExpressionContext(BaseExpressionContext): def __init__(self,appdaemon_api, sensor_existence_validation = True): @@ -13,6 +13,7 @@ class ADExpressionContext(BaseExpressionContext): self.sensors_default_value = dict() self.sensor_states = dict() self.sensor_existence_validation = sensor_existence_validation + self.local_expressions = {} self.declare_default_constants() @@ -131,7 +132,22 @@ class ADExpressionContext(BaseExpressionContext): def get_entities_to_listen(self): 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 is_a_sensor(string): diff --git a/smartcondition.py b/smartcondition.py index 9d5c1ea..2116aaf 100644 --- a/smartcondition.py +++ b/smartcondition.py @@ -31,6 +31,7 @@ class Evaluator(): __LOG_INIT = "log_init" __EXTRA_ENTITIES_TO_LISTEN = "extra_entities_to_listen" __CONSTANTS = "constants" + __LOCAL_EXPRESSIONS_STRING = "local_expressions" __UNAVAIBILITY_RESULT = "unavaibility_result" def __init__(self, appdaemon_api, conditions_block,**kwargs): @@ -313,6 +314,12 @@ class Evaluator(): if 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): self.__log(f"Template '{template}' contain recursive template",level = "ERROR") @@ -377,7 +384,19 @@ class Evaluator(): if self.__log_init: self.__log(f"{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) if trigger_conditions_args: diff --git a/unit_tests/test_template.yaml b/unit_tests/test_template.yaml index a74cf33..bffa665 100644 --- a/unit_tests/test_template.yaml +++ b/unit_tests/test_template.yaml @@ -23,6 +23,10 @@ templates_library: trigger_conditions: binary_sensor.off == False blocking_conditions: - binary_sensor.on + T4: + local_expressions: + local_threshold: "binary_sensor.true ? 170 : 200" + trigger_conditions: sensor.int170 == local_threshold tests: test_1: @@ -44,3 +48,25 @@ tests: conditions: template_conditions: , 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: + + # 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