mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'modbus_func_ptr' into integration
This commit is contained in:
		| @@ -62,6 +62,7 @@ from esphome.cpp_types import (  # noqa: F401 | ||||
|     EntityBase, | ||||
|     EntityCategory, | ||||
|     ESPTime, | ||||
|     FixedVector, | ||||
|     GPIOPin, | ||||
|     InternalGPIOPin, | ||||
|     JsonObject, | ||||
|   | ||||
| @@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = { | ||||
|     "int": cg.int32, | ||||
|     "float": float, | ||||
|     "string": cg.std_string, | ||||
|     "bool[]": cg.std_vector.template(bool), | ||||
|     "int[]": cg.std_vector.template(cg.int32), | ||||
|     "float[]": cg.std_vector.template(float), | ||||
|     "string[]": cg.std_vector.template(cg.std_string), | ||||
|     "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"), | ||||
|     "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), | ||||
|     "float[]": cg.FixedVector.template(float).operator("const").operator("ref"), | ||||
|     "string[]": cg.FixedVector.template(cg.std_string) | ||||
|     .operator("const") | ||||
|     .operator("ref"), | ||||
| } | ||||
| CONF_ENCRYPTION = "encryption" | ||||
| CONF_BATCH_DELAY = "batch_delay" | ||||
|   | ||||
| @@ -11,23 +11,58 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument & | ||||
| } | ||||
| template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; } | ||||
| template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; } | ||||
|  | ||||
| // Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve | ||||
| template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) { | ||||
|   return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end()); | ||||
|   std::vector<bool> result; | ||||
|   result.reserve(arg.bool_array.size()); | ||||
|   result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end()); | ||||
|   return result; | ||||
| } | ||||
| template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) { | ||||
|   return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end()); | ||||
|   std::vector<int32_t> result; | ||||
|   result.reserve(arg.int_array.size()); | ||||
|   result.insert(result.end(), arg.int_array.begin(), arg.int_array.end()); | ||||
|   return result; | ||||
| } | ||||
| template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) { | ||||
|   return std::vector<float>(arg.float_array.begin(), arg.float_array.end()); | ||||
|   std::vector<float> result; | ||||
|   result.reserve(arg.float_array.size()); | ||||
|   result.insert(result.end(), arg.float_array.begin(), arg.float_array.end()); | ||||
|   return result; | ||||
| } | ||||
| template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) { | ||||
|   return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end()); | ||||
|   std::vector<std::string> result; | ||||
|   result.reserve(arg.string_array.size()); | ||||
|   result.insert(result.end(), arg.string_array.begin(), arg.string_array.end()); | ||||
|   return result; | ||||
| } | ||||
|  | ||||
| // New FixedVector const reference versions for YAML-generated services - zero-copy | ||||
| template<> | ||||
| const FixedVector<bool> &get_execute_arg_value<const FixedVector<bool> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.bool_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<int32_t> &get_execute_arg_value<const FixedVector<int32_t> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.int_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<float> &get_execute_arg_value<const FixedVector<float> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.float_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<std::string> &get_execute_arg_value<const FixedVector<std::string> &>( | ||||
|     const ExecuteServiceArgument &arg) { | ||||
|   return arg.string_array; | ||||
| } | ||||
|  | ||||
| template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; } | ||||
|  | ||||
| // Legacy std::vector versions for external components using custom_api_device.h | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() { | ||||
|   return enums::SERVICE_ARG_TYPE_INT_ARRAY; | ||||
| @@ -39,4 +74,18 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>() | ||||
|   return enums::SERVICE_ARG_TYPE_STRING_ARRAY; | ||||
| } | ||||
|  | ||||
| // New FixedVector const reference versions for YAML-generated services | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<bool> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<int32_t> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_INT_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<float> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<std::string> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_STRING_ARRAY; | ||||
| } | ||||
|  | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -33,8 +33,8 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
|   using transform_func_t = std::function<optional<bool>(ModbusBinarySensor *, bool, const std::vector<uint8_t> &)>; | ||||
|   void set_template(transform_func_t &&f) { this->transform_func_ = f; } | ||||
|   using transform_func_t = optional<bool> (*)(ModbusBinarySensor *, bool, const std::vector<uint8_t> &); | ||||
|   void set_template(transform_func_t f) { this->transform_func_ = f; } | ||||
|  | ||||
|  protected: | ||||
|   optional<transform_func_t> transform_func_{nullopt}; | ||||
|   | ||||
| @@ -31,10 +31,10 @@ class ModbusNumber : public number::Number, public Component, public SensorItem | ||||
|   void set_parent(ModbusController *parent) { this->parent_ = parent; } | ||||
|   void set_write_multiply(float factor) { this->multiply_by_ = factor; } | ||||
|  | ||||
|   using transform_func_t = std::function<optional<float>(ModbusNumber *, float, const std::vector<uint8_t> &)>; | ||||
|   using write_transform_func_t = std::function<optional<float>(ModbusNumber *, float, std::vector<uint16_t> &)>; | ||||
|   void set_template(transform_func_t &&f) { this->transform_func_ = f; } | ||||
|   void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } | ||||
|   using transform_func_t = optional<float> (*)(ModbusNumber *, float, const std::vector<uint8_t> &); | ||||
|   using write_transform_func_t = optional<float> (*)(ModbusNumber *, float, std::vector<uint16_t> &); | ||||
|   void set_template(transform_func_t f) { this->transform_func_ = f; } | ||||
|   void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } | ||||
|   void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } | ||||
|  | ||||
|  protected: | ||||
|   | ||||
| @@ -29,8 +29,8 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S | ||||
|   // Do nothing | ||||
|   void parse_and_publish(const std::vector<uint8_t> &data) override{}; | ||||
|  | ||||
|   using write_transform_func_t = std::function<optional<float>(ModbusFloatOutput *, float, std::vector<uint16_t> &)>; | ||||
|   void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } | ||||
|   using write_transform_func_t = optional<float> (*)(ModbusFloatOutput *, float, std::vector<uint16_t> &); | ||||
|   void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } | ||||
|   void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } | ||||
|  | ||||
|  protected: | ||||
| @@ -60,8 +60,8 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public | ||||
|   // Do nothing | ||||
|   void parse_and_publish(const std::vector<uint8_t> &data) override{}; | ||||
|  | ||||
|   using write_transform_func_t = std::function<optional<bool>(ModbusBinaryOutput *, bool, std::vector<uint8_t> &)>; | ||||
|   void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } | ||||
|   using write_transform_func_t = optional<bool> (*)(ModbusBinaryOutput *, bool, std::vector<uint8_t> &); | ||||
|   void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } | ||||
|   void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } | ||||
|  | ||||
|  protected: | ||||
|   | ||||
| @@ -26,16 +26,15 @@ class ModbusSelect : public Component, public select::Select, public SensorItem | ||||
|     this->mapping_ = std::move(mapping); | ||||
|   } | ||||
|  | ||||
|   using transform_func_t = | ||||
|       std::function<optional<std::string>(ModbusSelect *const, int64_t, const std::vector<uint8_t> &)>; | ||||
|   using write_transform_func_t = | ||||
|       std::function<optional<int64_t>(ModbusSelect *const, const std::string &, int64_t, std::vector<uint16_t> &)>; | ||||
|   using transform_func_t = optional<std::string> (*)(ModbusSelect *const, int64_t, const std::vector<uint8_t> &); | ||||
|   using write_transform_func_t = optional<int64_t> (*)(ModbusSelect *const, const std::string &, int64_t, | ||||
|                                                        std::vector<uint16_t> &); | ||||
|  | ||||
|   void set_parent(ModbusController *const parent) { this->parent_ = parent; } | ||||
|   void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } | ||||
|   void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
|   void set_template(transform_func_t &&f) { this->transform_func_ = f; } | ||||
|   void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } | ||||
|   void set_template(transform_func_t f) { this->transform_func_ = f; } | ||||
|   void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } | ||||
|  | ||||
|   void dump_config() override; | ||||
|   void parse_and_publish(const std::vector<uint8_t> &data) override; | ||||
|   | ||||
| @@ -25,9 +25,9 @@ class ModbusSensor : public Component, public sensor::Sensor, public SensorItem | ||||
|  | ||||
|   void parse_and_publish(const std::vector<uint8_t> &data) override; | ||||
|   void dump_config() override; | ||||
|   using transform_func_t = std::function<optional<float>(ModbusSensor *, float, const std::vector<uint8_t> &)>; | ||||
|   using transform_func_t = optional<float> (*)(ModbusSensor *, float, const std::vector<uint8_t> &); | ||||
|  | ||||
|   void set_template(transform_func_t &&f) { this->transform_func_ = f; } | ||||
|   void set_template(transform_func_t f) { this->transform_func_ = f; } | ||||
|  | ||||
|  protected: | ||||
|   optional<transform_func_t> transform_func_{nullopt}; | ||||
|   | ||||
| @@ -34,10 +34,10 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem | ||||
|   void parse_and_publish(const std::vector<uint8_t> &data) override; | ||||
|   void set_parent(ModbusController *parent) { this->parent_ = parent; } | ||||
|  | ||||
|   using transform_func_t = std::function<optional<bool>(ModbusSwitch *, bool, const std::vector<uint8_t> &)>; | ||||
|   using write_transform_func_t = std::function<optional<bool>(ModbusSwitch *, bool, std::vector<uint8_t> &)>; | ||||
|   void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; } | ||||
|   void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } | ||||
|   using transform_func_t = optional<bool> (*)(ModbusSwitch *, bool, const std::vector<uint8_t> &); | ||||
|   using write_transform_func_t = optional<bool> (*)(ModbusSwitch *, bool, std::vector<uint8_t> &); | ||||
|   void set_template(transform_func_t f) { this->publish_transform_func_ = f; } | ||||
|   void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } | ||||
|   void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } | ||||
|  | ||||
|  protected: | ||||
|   | ||||
| @@ -30,9 +30,8 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void parse_and_publish(const std::vector<uint8_t> &data) override; | ||||
|   using transform_func_t = | ||||
|       std::function<optional<std::string>(ModbusTextSensor *, std::string, const std::vector<uint8_t> &)>; | ||||
|   void set_template(transform_func_t &&f) { this->transform_func_ = f; } | ||||
|   using transform_func_t = optional<std::string> (*)(ModbusTextSensor *, std::string, const std::vector<uint8_t> &); | ||||
|   void set_template(transform_func_t f) { this->transform_func_ = f; } | ||||
|  | ||||
|  protected: | ||||
|   optional<transform_func_t> transform_func_{nullopt}; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| import logging | ||||
| from re import Match | ||||
| from typing import Any | ||||
|  | ||||
| from esphome import core | ||||
| from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered | ||||
| @@ -39,7 +41,34 @@ async def to_code(config): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| def _expand_jinja(value, orig_value, path, jinja, ignore_missing): | ||||
| def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBase: | ||||
|     """This function restores ESPHomeDataBase metadata held by the original string. | ||||
|     This is needed because during jinja evaluation, strings can be replaced by other types, | ||||
|     but we want to keep the original metadata for error reporting and source mapping. | ||||
|     For example, if a substitution replaces a string with a dictionary, we want that items | ||||
|     in the dictionary to still point to the original document location | ||||
|     """ | ||||
|     if isinstance(value, ESPHomeDataBase): | ||||
|         return value | ||||
|     if isinstance(value, dict): | ||||
|         return { | ||||
|             _restore_data_base(k, orig_value): _restore_data_base(v, orig_value) | ||||
|             for k, v in value.items() | ||||
|         } | ||||
|     if isinstance(value, list): | ||||
|         return [_restore_data_base(v, orig_value) for v in value] | ||||
|     if isinstance(value, str): | ||||
|         return make_data_base(value, orig_value) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def _expand_jinja( | ||||
|     value: str | JinjaStr, | ||||
|     orig_value: str | JinjaStr, | ||||
|     path, | ||||
|     jinja: Jinja, | ||||
|     ignore_missing: bool, | ||||
| ) -> Any: | ||||
|     if has_jinja(value): | ||||
|         # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved | ||||
|         # Jinja expression from a previous pass. | ||||
| @@ -65,10 +94,17 @@ def _expand_jinja(value, orig_value, path, jinja, ignore_missing): | ||||
|                 f"\nSee {'->'.join(str(x) for x in path)}", | ||||
|                 path, | ||||
|             ) | ||||
|         # If the original, unexpanded string, contained document metadata (ESPHomeDatabase), | ||||
|         # assign this same document metadata to the resulting value. | ||||
|         if isinstance(orig_value, ESPHomeDataBase): | ||||
|             value = _restore_data_base(value, orig_value) | ||||
|  | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): | ||||
| def _expand_substitutions( | ||||
|     substitutions: dict, value: str, path, jinja: Jinja, ignore_missing: bool | ||||
| ) -> Any: | ||||
|     if "$" not in value: | ||||
|         return value | ||||
|  | ||||
| @@ -76,14 +112,14 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): | ||||
|  | ||||
|     i = 0 | ||||
|     while True: | ||||
|         m = cv.VARIABLE_PROG.search(value, i) | ||||
|         m: Match[str] = cv.VARIABLE_PROG.search(value, i) | ||||
|         if not m: | ||||
|             # No more variable substitutions found. See if the remainder looks like a jinja template | ||||
|             value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) | ||||
|             break | ||||
|  | ||||
|         i, j = m.span(0) | ||||
|         name = m.group(1) | ||||
|         name: str = m.group(1) | ||||
|         if name.startswith("{") and name.endswith("}"): | ||||
|             name = name[1:-1] | ||||
|         if name not in substitutions: | ||||
| @@ -98,7 +134,7 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): | ||||
|             i = j | ||||
|             continue | ||||
|  | ||||
|         sub = substitutions[name] | ||||
|         sub: Any = substitutions[name] | ||||
|  | ||||
|         if i == 0 and j == len(value): | ||||
|             # The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly | ||||
| @@ -121,7 +157,13 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def _substitute_item(substitutions, item, path, jinja, ignore_missing): | ||||
| def _substitute_item( | ||||
|     substitutions: dict, | ||||
|     item: Any, | ||||
|     path: list[int | str], | ||||
|     jinja: Jinja, | ||||
|     ignore_missing: bool, | ||||
| ) -> Any | None: | ||||
|     if isinstance(item, ESPLiteralValue): | ||||
|         return None  # do not substitute inside literal blocks | ||||
|     if isinstance(item, list): | ||||
| @@ -160,7 +202,9 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing): | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def do_substitution_pass(config, command_line_substitutions, ignore_missing=False): | ||||
| def do_substitution_pass( | ||||
|     config: dict, command_line_substitutions: dict, ignore_missing: bool = False | ||||
| ) -> None: | ||||
|     if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: | ||||
|         return | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,14 @@ | ||||
| from ast import literal_eval | ||||
| from collections.abc import Iterator | ||||
| from itertools import chain, islice | ||||
| import logging | ||||
| import math | ||||
| import re | ||||
| from types import GeneratorType | ||||
| from typing import Any | ||||
|  | ||||
| import jinja2 as jinja | ||||
| from jinja2.sandbox import SandboxedEnvironment | ||||
| from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate | ||||
|  | ||||
| from esphome.yaml_util import ESPLiteralValue | ||||
|  | ||||
| @@ -24,7 +28,7 @@ detect_jinja_re = re.compile( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def has_jinja(st): | ||||
| def has_jinja(st: str) -> bool: | ||||
|     return detect_jinja_re.search(st) is not None | ||||
|  | ||||
|  | ||||
| @@ -109,12 +113,56 @@ class TrackerContext(jinja.runtime.Context): | ||||
|         return val | ||||
|  | ||||
|  | ||||
| class Jinja(SandboxedEnvironment): | ||||
| def _concat_nodes_override(values: Iterator[Any]) -> Any: | ||||
|     """ | ||||
|     This function customizes how Jinja preserves native types when concatenating | ||||
|     multiple result nodes together. If the result is a single node, its value | ||||
|     is returned. Otherwise, the nodes are concatenated as strings. If | ||||
|     the result can be parsed with `ast.literal_eval`, the parsed | ||||
|     value is returned. Otherwise, the string is returned. | ||||
|     This helps preserve metadata such as ESPHomeDataBase from original values | ||||
|     and mimicks how HomeAssistant deals with template evaluation and preserving | ||||
|     the original datatype. | ||||
|     """ | ||||
|     head: list[Any] = list(islice(values, 2)) | ||||
|  | ||||
|     if not head: | ||||
|         return None | ||||
|  | ||||
|     if len(head) == 1: | ||||
|         raw = head[0] | ||||
|         if not isinstance(raw, str): | ||||
|             return raw | ||||
|     else: | ||||
|         if isinstance(values, GeneratorType): | ||||
|             values = chain(head, values) | ||||
|         raw = "".join([str(v) for v in values]) | ||||
|  | ||||
|     try: | ||||
|         # Attempt to parse the concatenated string into a Python literal. | ||||
|         # This allows expressions like "1 + 2" to be evaluated to the integer 3. | ||||
|         # If the result is also a string or there is a parsing error, | ||||
|         # fall back to returning the raw string. This is consistent with | ||||
|         #  Home Assistant's behavior when evaluating templates | ||||
|         result = literal_eval(raw) | ||||
|         if not isinstance(result, str): | ||||
|             return result | ||||
|  | ||||
|     except (ValueError, SyntaxError, MemoryError, TypeError): | ||||
|         pass | ||||
|     return raw | ||||
|  | ||||
|  | ||||
| class Jinja(jinja.Environment): | ||||
|     """ | ||||
|     Wraps a Jinja environment | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, context_vars): | ||||
|     # jinja environment customization overrides | ||||
|     code_generator_class = NativeCodeGenerator | ||||
|     concat = staticmethod(_concat_nodes_override) | ||||
|  | ||||
|     def __init__(self, context_vars: dict): | ||||
|         super().__init__( | ||||
|             trim_blocks=True, | ||||
|             lstrip_blocks=True, | ||||
| @@ -142,19 +190,10 @@ class Jinja(SandboxedEnvironment): | ||||
|             **SAFE_GLOBALS, | ||||
|         } | ||||
|  | ||||
|     def safe_eval(self, expr): | ||||
|         try: | ||||
|             result = literal_eval(expr) | ||||
|             if not isinstance(result, str): | ||||
|                 return result | ||||
|         except (ValueError, SyntaxError, MemoryError, TypeError): | ||||
|             pass | ||||
|         return expr | ||||
|  | ||||
|     def expand(self, content_str): | ||||
|     def expand(self, content_str: str | JinjaStr) -> Any: | ||||
|         """ | ||||
|         Renders a string that may contain Jinja expressions or statements | ||||
|         Returns the resulting processed string if all values could be resolved. | ||||
|         Returns the resulting value if all variables and expressions could be resolved. | ||||
|         Otherwise, it returns a tagged (JinjaStr) string that captures variables | ||||
|         in scope (upvalues), like a closure for later evaluation. | ||||
|         """ | ||||
| @@ -172,7 +211,7 @@ class Jinja(SandboxedEnvironment): | ||||
|         self.context_trace = {} | ||||
|         try: | ||||
|             template = self.from_string(content_str) | ||||
|             result = self.safe_eval(template.render(override_vars)) | ||||
|             result = template.render(override_vars) | ||||
|             if isinstance(result, Undefined): | ||||
|                 print("" + result)  # force a UndefinedError exception | ||||
|         except (TemplateSyntaxError, UndefinedError) as err: | ||||
| @@ -201,3 +240,10 @@ class Jinja(SandboxedEnvironment): | ||||
|             content_str.result = result | ||||
|  | ||||
|         return result, None | ||||
|  | ||||
|  | ||||
| class JinjaTemplate(NativeTemplate): | ||||
|     environment_class = Jinja | ||||
|  | ||||
|  | ||||
| Jinja.template_class = JinjaTemplate | ||||
|   | ||||
| @@ -99,10 +99,26 @@ void IDFUARTComponent::setup() { | ||||
| } | ||||
|  | ||||
| void IDFUARTComponent::load_settings(bool dump_config) { | ||||
|   uart_config_t uart_config = this->get_config_(); | ||||
|   esp_err_t err = uart_param_config(this->uart_num_, &uart_config); | ||||
|   esp_err_t err; | ||||
|  | ||||
|   if (uart_is_driver_installed(this->uart_num_)) { | ||||
|     err = uart_driver_delete(this->uart_num_); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   err = uart_driver_install(this->uart_num_,        // UART number | ||||
|                             this->rx_buffer_size_,  // RX ring buffer size | ||||
|                             0,   // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will | ||||
|                                  // block task until all data has been sent out | ||||
|                             20,  // event queue size/depth | ||||
|                             &this->uart_event_queue_,  // event queue | ||||
|                             0                          // Flags used to allocate the interrupt | ||||
|   ); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); | ||||
|     ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| @@ -119,10 +135,12 @@ void IDFUARTComponent::load_settings(bool dump_config) { | ||||
|   int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; | ||||
|  | ||||
|   uint32_t invert = 0; | ||||
|   if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) | ||||
|   if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { | ||||
|     invert |= UART_SIGNAL_TXD_INV; | ||||
|   if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) | ||||
|   } | ||||
|   if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) { | ||||
|     invert |= UART_SIGNAL_RXD_INV; | ||||
|   } | ||||
|  | ||||
|   err = uart_set_line_inverse(this->uart_num_, invert); | ||||
|   if (err != ESP_OK) { | ||||
| @@ -138,26 +156,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (uart_is_driver_installed(this->uart_num_)) { | ||||
|     uart_driver_delete(this->uart_num_); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, | ||||
|                             /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will | ||||
|                                block task until all data have been sent out.*/ | ||||
|                             0, | ||||
|                             /* UART event queue size/depth. */ 20, &(this->uart_event_queue_), | ||||
|                             /* Flags used to allocate the interrupt. */ 0); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err)); | ||||
| @@ -173,24 +171,32 @@ void IDFUARTComponent::load_settings(bool dump_config) { | ||||
|   } | ||||
|  | ||||
|   auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; | ||||
|   err = uart_set_mode(this->uart_num_, mode); | ||||
|   err = uart_set_mode(this->uart_num_, mode);  // per docs, must be called only after uart_driver_install() | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   uart_config_t uart_config = this->get_config_(); | ||||
|   err = uart_param_config(this->uart_num_, &uart_config); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (dump_config) { | ||||
|     ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->uart_num_); | ||||
|     ESP_LOGCONFIG(TAG, "Reloaded UART %u", this->uart_num_); | ||||
|     this->dump_config(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void IDFUARTComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); | ||||
|   LOG_PIN("  TX Pin: ", tx_pin_); | ||||
|   LOG_PIN("  RX Pin: ", rx_pin_); | ||||
|   LOG_PIN("  Flow Control Pin: ", flow_control_pin_); | ||||
|   LOG_PIN("  TX Pin: ", this->tx_pin_); | ||||
|   LOG_PIN("  RX Pin: ", this->rx_pin_); | ||||
|   LOG_PIN("  Flow Control Pin: ", this->flow_control_pin_); | ||||
|   if (this->rx_pin_ != nullptr) { | ||||
|     ESP_LOGCONFIG(TAG, | ||||
|                   "  RX Buffer Size: %u\n" | ||||
|   | ||||
| @@ -23,6 +23,7 @@ size_t = global_ns.namespace("size_t") | ||||
| const_char_ptr = global_ns.namespace("const char *") | ||||
| NAN = global_ns.namespace("NAN") | ||||
| esphome_ns = global_ns  # using namespace esphome; | ||||
| FixedVector = esphome_ns.class_("FixedVector") | ||||
| App = esphome_ns.App | ||||
| EntityBase = esphome_ns.class_("EntityBase") | ||||
| Component = esphome_ns.class_("Component") | ||||
|   | ||||
| @@ -120,7 +120,7 @@ def prepare( | ||||
|                 cert_file.flush() | ||||
|                 key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) | ||||
|                 key_file.flush() | ||||
|                 context.load_cert_chain(cert_file, key_file) | ||||
|                 context.load_cert_chain(cert_file.name, key_file.name) | ||||
|         client.tls_set_context(context) | ||||
|  | ||||
|     try: | ||||
|   | ||||
| @@ -56,6 +56,14 @@ binary_sensor: | ||||
|     register_type: read | ||||
|     address: 0x3200 | ||||
|     bitmask: 0x80 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_binary_sensor2 | ||||
|     name: Test Binary Sensor with Lambda | ||||
|     register_type: read | ||||
|     address: 0x3201 | ||||
|     lambda: |- | ||||
|       return x; | ||||
|  | ||||
| number: | ||||
|   - platform: modbus_controller | ||||
| @@ -65,6 +73,16 @@ number: | ||||
|     address: 0x9001 | ||||
|     value_type: U_WORD | ||||
|     multiply: 1.0 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_number2 | ||||
|     name: Test Number with Lambda | ||||
|     address: 0x9002 | ||||
|     value_type: U_WORD | ||||
|     lambda: |- | ||||
|       return x * 2.0; | ||||
|     write_lambda: |- | ||||
|       return x / 2.0; | ||||
|  | ||||
| output: | ||||
|   - platform: modbus_controller | ||||
| @@ -74,6 +92,14 @@ output: | ||||
|     register_type: holding | ||||
|     value_type: U_WORD | ||||
|     multiply: 1000 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_output2 | ||||
|     address: 2049 | ||||
|     register_type: holding | ||||
|     value_type: U_WORD | ||||
|     write_lambda: |- | ||||
|       return x * 100.0; | ||||
|  | ||||
| select: | ||||
|   - platform: modbus_controller | ||||
| @@ -87,6 +113,34 @@ select: | ||||
|       "One": 1 | ||||
|       "Two": 2 | ||||
|       "Three": 3 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_select2 | ||||
|     name: Test Select with Lambda | ||||
|     address: 1001 | ||||
|     value_type: U_WORD | ||||
|     optionsmap: | ||||
|       "Off": 0 | ||||
|       "On": 1 | ||||
|       "Two": 2 | ||||
|     lambda: |- | ||||
|       ESP_LOGD("Reg1001", "Received value %lld", x); | ||||
|       if (x > 1) { | ||||
|         return std::string("Two"); | ||||
|       } else if (x == 1) { | ||||
|         return std::string("On"); | ||||
|       } | ||||
|       return std::string("Off"); | ||||
|     write_lambda: |- | ||||
|       ESP_LOGD("Reg1001", "Set option to %s (%lld)", x.c_str(), value); | ||||
|       if (x == "On") { | ||||
|         return 1; | ||||
|       } | ||||
|       if (x == "Two") { | ||||
|         payload.push_back(0x0002); | ||||
|         return 0; | ||||
|       } | ||||
|       return value; | ||||
|  | ||||
| sensor: | ||||
|   - platform: modbus_controller | ||||
| @@ -97,6 +151,15 @@ sensor: | ||||
|     address: 0x9001 | ||||
|     unit_of_measurement: "AH" | ||||
|     value_type: U_WORD | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_sensor2 | ||||
|     name: Test Sensor with Lambda | ||||
|     register_type: holding | ||||
|     address: 0x9002 | ||||
|     value_type: U_WORD | ||||
|     lambda: |- | ||||
|       return x / 10.0; | ||||
|  | ||||
| switch: | ||||
|   - platform: modbus_controller | ||||
| @@ -106,6 +169,16 @@ switch: | ||||
|     register_type: coil | ||||
|     address: 0x15 | ||||
|     bitmask: 1 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_switch2 | ||||
|     name: Test Switch with Lambda | ||||
|     register_type: coil | ||||
|     address: 0x16 | ||||
|     lambda: |- | ||||
|       return !x; | ||||
|     write_lambda: |- | ||||
|       return !x; | ||||
|  | ||||
| text_sensor: | ||||
|   - platform: modbus_controller | ||||
| @@ -117,3 +190,13 @@ text_sensor: | ||||
|     register_count: 3 | ||||
|     raw_encode: HEXBYTES | ||||
|     response_size: 6 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_text_sensor2 | ||||
|     name: Test Text Sensor with Lambda | ||||
|     register_type: holding | ||||
|     address: 0x9014 | ||||
|     register_count: 2 | ||||
|     response_size: 4 | ||||
|     lambda: |- | ||||
|       return "Modified: " + x; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import glob | ||||
| import logging | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
|  | ||||
| from esphome import config as config_module, yaml_util | ||||
| from esphome.components import substitutions | ||||
| @@ -60,6 +61,29 @@ def write_yaml(path: Path, data: dict) -> None: | ||||
|     path.write_text(yaml_util.dump(data), encoding="utf-8") | ||||
|  | ||||
|  | ||||
| def verify_database(value: Any, path: str = "") -> str | None: | ||||
|     if isinstance(value, list): | ||||
|         for i, v in enumerate(value): | ||||
|             result = verify_database(v, f"{path}[{i}]") | ||||
|             if result is not None: | ||||
|                 return result | ||||
|         return None | ||||
|     if isinstance(value, dict): | ||||
|         for k, v in value.items(): | ||||
|             key_result = verify_database(k, f"{path}/{k}") | ||||
|             if key_result is not None: | ||||
|                 return key_result | ||||
|             value_result = verify_database(v, f"{path}/{k}") | ||||
|             if value_result is not None: | ||||
|                 return value_result | ||||
|         return None | ||||
|     if isinstance(value, str): | ||||
|         if not isinstance(value, yaml_util.ESPHomeDataBase): | ||||
|             return f"{path}: {value!r} is not ESPHomeDataBase" | ||||
|         return None | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def test_substitutions_fixtures(fixture_path): | ||||
|     base_dir = fixture_path / "substitutions" | ||||
|     sources = sorted(glob.glob(str(base_dir / "*.input.yaml"))) | ||||
| @@ -83,6 +107,9 @@ def test_substitutions_fixtures(fixture_path): | ||||
|             substitutions.do_substitution_pass(config, None) | ||||
|  | ||||
|             resolve_extend_remove(config) | ||||
|             verify_database_result = verify_database(config) | ||||
|             if verify_database_result is not None: | ||||
|                 raise AssertionError(verify_database_result) | ||||
|  | ||||
|             # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE | ||||
|             if expected_path.is_file(): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user