mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 23:21:54 +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, |     EntityBase, | ||||||
|     EntityCategory, |     EntityCategory, | ||||||
|     ESPTime, |     ESPTime, | ||||||
|  |     FixedVector, | ||||||
|     GPIOPin, |     GPIOPin, | ||||||
|     InternalGPIOPin, |     InternalGPIOPin, | ||||||
|     JsonObject, |     JsonObject, | ||||||
|   | |||||||
| @@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = { | |||||||
|     "int": cg.int32, |     "int": cg.int32, | ||||||
|     "float": float, |     "float": float, | ||||||
|     "string": cg.std_string, |     "string": cg.std_string, | ||||||
|     "bool[]": cg.std_vector.template(bool), |     "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"), | ||||||
|     "int[]": cg.std_vector.template(cg.int32), |     "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), | ||||||
|     "float[]": cg.std_vector.template(float), |     "float[]": cg.FixedVector.template(float).operator("const").operator("ref"), | ||||||
|     "string[]": cg.std_vector.template(cg.std_string), |     "string[]": cg.FixedVector.template(cg.std_string) | ||||||
|  |     .operator("const") | ||||||
|  |     .operator("ref"), | ||||||
| } | } | ||||||
| CONF_ENCRYPTION = "encryption" | CONF_ENCRYPTION = "encryption" | ||||||
| CONF_BATCH_DELAY = "batch_delay" | 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<> 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_; } | 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) { | 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) { | 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) { | 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) { | 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<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<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<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; } | ||||||
| template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; } | 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<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } | ||||||
| template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() { | template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() { | ||||||
|   return enums::SERVICE_ARG_TYPE_INT_ARRAY; |   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; |   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 | }  // namespace esphome::api | ||||||
|   | |||||||
| @@ -33,8 +33,8 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, | |||||||
|  |  | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|  |  | ||||||
|   using transform_func_t = std::function<optional<bool>(ModbusBinarySensor *, bool, const std::vector<uint8_t> &)>; |   using transform_func_t = optional<bool> (*)(ModbusBinarySensor *, bool, 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: |  protected: | ||||||
|   optional<transform_func_t> transform_func_{nullopt}; |   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_parent(ModbusController *parent) { this->parent_ = parent; } | ||||||
|   void set_write_multiply(float factor) { this->multiply_by_ = factor; } |   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 transform_func_t = optional<float> (*)(ModbusNumber *, float, const std::vector<uint8_t> &); | ||||||
|   using write_transform_func_t = std::function<optional<float>(ModbusNumber *, float, std::vector<uint16_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_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_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; } |   void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   | |||||||
| @@ -29,8 +29,8 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S | |||||||
|   // Do nothing |   // Do nothing | ||||||
|   void parse_and_publish(const std::vector<uint8_t> &data) override{}; |   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> &)>; |   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_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; } |   void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| @@ -60,8 +60,8 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public | |||||||
|   // Do nothing |   // Do nothing | ||||||
|   void parse_and_publish(const std::vector<uint8_t> &data) override{}; |   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> &)>; |   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_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; } |   void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   | |||||||
| @@ -26,16 +26,15 @@ class ModbusSelect : public Component, public select::Select, public SensorItem | |||||||
|     this->mapping_ = std::move(mapping); |     this->mapping_ = std::move(mapping); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   using transform_func_t = |   using transform_func_t = optional<std::string> (*)(ModbusSelect *const, int64_t, const std::vector<uint8_t> &); | ||||||
|       std::function<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, | ||||||
|   using write_transform_func_t = |                                                        std::vector<uint16_t> &); | ||||||
|       std::function<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_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_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } | ||||||
|   void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } |   void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||||
|   void set_template(transform_func_t &&f) { this->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 set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } | ||||||
|  |  | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void parse_and_publish(const std::vector<uint8_t> &data) 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 parse_and_publish(const std::vector<uint8_t> &data) override; | ||||||
|   void dump_config() 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: |  protected: | ||||||
|   optional<transform_func_t> transform_func_{nullopt}; |   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 parse_and_publish(const std::vector<uint8_t> &data) override; | ||||||
|   void set_parent(ModbusController *parent) { this->parent_ = parent; } |   void set_parent(ModbusController *parent) { this->parent_ = parent; } | ||||||
|  |  | ||||||
|   using transform_func_t = std::function<optional<bool>(ModbusSwitch *, bool, const std::vector<uint8_t> &)>; |   using transform_func_t = optional<bool> (*)(ModbusSwitch *, bool, const std::vector<uint8_t> &); | ||||||
|   using write_transform_func_t = std::function<optional<bool>(ModbusSwitch *, bool, 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_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_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; } |   void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   | |||||||
| @@ -30,9 +30,8 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi | |||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|  |  | ||||||
|   void parse_and_publish(const std::vector<uint8_t> &data) override; |   void parse_and_publish(const std::vector<uint8_t> &data) override; | ||||||
|   using transform_func_t = |   using transform_func_t = optional<std::string> (*)(ModbusTextSensor *, std::string, const std::vector<uint8_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; } | ||||||
|   void set_template(transform_func_t &&f) { this->transform_func_ = f; } |  | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   optional<transform_func_t> transform_func_{nullopt}; |   optional<transform_func_t> transform_func_{nullopt}; | ||||||
|   | |||||||
| @@ -1,4 +1,6 @@ | |||||||
| import logging | import logging | ||||||
|  | from re import Match | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
| from esphome import core | from esphome import core | ||||||
| from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered | from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered | ||||||
| @@ -39,7 +41,34 @@ async def to_code(config): | |||||||
|     pass |     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 has_jinja(value): | ||||||
|         # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved |         # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved | ||||||
|         # Jinja expression from a previous pass. |         # 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)}", |                 f"\nSee {'->'.join(str(x) for x in path)}", | ||||||
|                 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 |     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: |     if "$" not in value: | ||||||
|         return value |         return value | ||||||
|  |  | ||||||
| @@ -76,14 +112,14 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): | |||||||
|  |  | ||||||
|     i = 0 |     i = 0 | ||||||
|     while True: |     while True: | ||||||
|         m = cv.VARIABLE_PROG.search(value, i) |         m: Match[str] = cv.VARIABLE_PROG.search(value, i) | ||||||
|         if not m: |         if not m: | ||||||
|             # No more variable substitutions found. See if the remainder looks like a jinja template |             # No more variable substitutions found. See if the remainder looks like a jinja template | ||||||
|             value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) |             value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) | ||||||
|             break |             break | ||||||
|  |  | ||||||
|         i, j = m.span(0) |         i, j = m.span(0) | ||||||
|         name = m.group(1) |         name: str = m.group(1) | ||||||
|         if name.startswith("{") and name.endswith("}"): |         if name.startswith("{") and name.endswith("}"): | ||||||
|             name = name[1:-1] |             name = name[1:-1] | ||||||
|         if name not in substitutions: |         if name not in substitutions: | ||||||
| @@ -98,7 +134,7 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): | |||||||
|             i = j |             i = j | ||||||
|             continue |             continue | ||||||
|  |  | ||||||
|         sub = substitutions[name] |         sub: Any = substitutions[name] | ||||||
|  |  | ||||||
|         if i == 0 and j == len(value): |         if i == 0 and j == len(value): | ||||||
|             # The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly |             # 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 |     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): |     if isinstance(item, ESPLiteralValue): | ||||||
|         return None  # do not substitute inside literal blocks |         return None  # do not substitute inside literal blocks | ||||||
|     if isinstance(item, list): |     if isinstance(item, list): | ||||||
| @@ -160,7 +202,9 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing): | |||||||
|     return None |     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: |     if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: | ||||||
|         return |         return | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,14 @@ | |||||||
| from ast import literal_eval | from ast import literal_eval | ||||||
|  | from collections.abc import Iterator | ||||||
|  | from itertools import chain, islice | ||||||
| import logging | import logging | ||||||
| import math | import math | ||||||
| import re | import re | ||||||
|  | from types import GeneratorType | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
| import jinja2 as jinja | import jinja2 as jinja | ||||||
| from jinja2.sandbox import SandboxedEnvironment | from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate | ||||||
|  |  | ||||||
| from esphome.yaml_util import ESPLiteralValue | 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 |     return detect_jinja_re.search(st) is not None | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -109,12 +113,56 @@ class TrackerContext(jinja.runtime.Context): | |||||||
|         return val |         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 |     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__( |         super().__init__( | ||||||
|             trim_blocks=True, |             trim_blocks=True, | ||||||
|             lstrip_blocks=True, |             lstrip_blocks=True, | ||||||
| @@ -142,19 +190,10 @@ class Jinja(SandboxedEnvironment): | |||||||
|             **SAFE_GLOBALS, |             **SAFE_GLOBALS, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     def safe_eval(self, expr): |     def expand(self, content_str: str | JinjaStr) -> Any: | ||||||
|         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): |  | ||||||
|         """ |         """ | ||||||
|         Renders a string that may contain Jinja expressions or statements |         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 |         Otherwise, it returns a tagged (JinjaStr) string that captures variables | ||||||
|         in scope (upvalues), like a closure for later evaluation. |         in scope (upvalues), like a closure for later evaluation. | ||||||
|         """ |         """ | ||||||
| @@ -172,7 +211,7 @@ class Jinja(SandboxedEnvironment): | |||||||
|         self.context_trace = {} |         self.context_trace = {} | ||||||
|         try: |         try: | ||||||
|             template = self.from_string(content_str) |             template = self.from_string(content_str) | ||||||
|             result = self.safe_eval(template.render(override_vars)) |             result = template.render(override_vars) | ||||||
|             if isinstance(result, Undefined): |             if isinstance(result, Undefined): | ||||||
|                 print("" + result)  # force a UndefinedError exception |                 print("" + result)  # force a UndefinedError exception | ||||||
|         except (TemplateSyntaxError, UndefinedError) as err: |         except (TemplateSyntaxError, UndefinedError) as err: | ||||||
| @@ -201,3 +240,10 @@ class Jinja(SandboxedEnvironment): | |||||||
|             content_str.result = result |             content_str.result = result | ||||||
|  |  | ||||||
|         return result, None |         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) { | void IDFUARTComponent::load_settings(bool dump_config) { | ||||||
|   uart_config_t uart_config = this->get_config_(); |   esp_err_t err; | ||||||
|   esp_err_t err = uart_param_config(this->uart_num_, &uart_config); |  | ||||||
|  |   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) { |   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(); |     this->mark_failed(); | ||||||
|     return; |     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; |   int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; | ||||||
|  |  | ||||||
|   uint32_t invert = 0; |   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; |     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; |     invert |= UART_SIGNAL_RXD_INV; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   err = uart_set_line_inverse(this->uart_num_, invert); |   err = uart_set_line_inverse(this->uart_num_, invert); | ||||||
|   if (err != ESP_OK) { |   if (err != ESP_OK) { | ||||||
| @@ -138,26 +156,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { | |||||||
|     return; |     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_); |   err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_); | ||||||
|   if (err != ESP_OK) { |   if (err != ESP_OK) { | ||||||
|     ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err)); |     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; |   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) { |   if (err != ESP_OK) { | ||||||
|     ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); |     ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); | ||||||
|     this->mark_failed(); |     this->mark_failed(); | ||||||
|     return; |     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) { |   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(); |     this->dump_config(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void IDFUARTComponent::dump_config() { | void IDFUARTComponent::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); |   ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); | ||||||
|   LOG_PIN("  TX Pin: ", tx_pin_); |   LOG_PIN("  TX Pin: ", this->tx_pin_); | ||||||
|   LOG_PIN("  RX Pin: ", rx_pin_); |   LOG_PIN("  RX Pin: ", this->rx_pin_); | ||||||
|   LOG_PIN("  Flow Control Pin: ", flow_control_pin_); |   LOG_PIN("  Flow Control Pin: ", this->flow_control_pin_); | ||||||
|   if (this->rx_pin_ != nullptr) { |   if (this->rx_pin_ != nullptr) { | ||||||
|     ESP_LOGCONFIG(TAG, |     ESP_LOGCONFIG(TAG, | ||||||
|                   "  RX Buffer Size: %u\n" |                   "  RX Buffer Size: %u\n" | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ size_t = global_ns.namespace("size_t") | |||||||
| const_char_ptr = global_ns.namespace("const char *") | const_char_ptr = global_ns.namespace("const char *") | ||||||
| NAN = global_ns.namespace("NAN") | NAN = global_ns.namespace("NAN") | ||||||
| esphome_ns = global_ns  # using namespace esphome; | esphome_ns = global_ns  # using namespace esphome; | ||||||
|  | FixedVector = esphome_ns.class_("FixedVector") | ||||||
| App = esphome_ns.App | App = esphome_ns.App | ||||||
| EntityBase = esphome_ns.class_("EntityBase") | EntityBase = esphome_ns.class_("EntityBase") | ||||||
| Component = esphome_ns.class_("Component") | Component = esphome_ns.class_("Component") | ||||||
|   | |||||||
| @@ -120,7 +120,7 @@ def prepare( | |||||||
|                 cert_file.flush() |                 cert_file.flush() | ||||||
|                 key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) |                 key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) | ||||||
|                 key_file.flush() |                 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) |         client.tls_set_context(context) | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|   | |||||||
| @@ -56,6 +56,14 @@ binary_sensor: | |||||||
|     register_type: read |     register_type: read | ||||||
|     address: 0x3200 |     address: 0x3200 | ||||||
|     bitmask: 0x80 |     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: | number: | ||||||
|   - platform: modbus_controller |   - platform: modbus_controller | ||||||
| @@ -65,6 +73,16 @@ number: | |||||||
|     address: 0x9001 |     address: 0x9001 | ||||||
|     value_type: U_WORD |     value_type: U_WORD | ||||||
|     multiply: 1.0 |     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: | output: | ||||||
|   - platform: modbus_controller |   - platform: modbus_controller | ||||||
| @@ -74,6 +92,14 @@ output: | |||||||
|     register_type: holding |     register_type: holding | ||||||
|     value_type: U_WORD |     value_type: U_WORD | ||||||
|     multiply: 1000 |     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: | select: | ||||||
|   - platform: modbus_controller |   - platform: modbus_controller | ||||||
| @@ -87,6 +113,34 @@ select: | |||||||
|       "One": 1 |       "One": 1 | ||||||
|       "Two": 2 |       "Two": 2 | ||||||
|       "Three": 3 |       "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: | sensor: | ||||||
|   - platform: modbus_controller |   - platform: modbus_controller | ||||||
| @@ -97,6 +151,15 @@ sensor: | |||||||
|     address: 0x9001 |     address: 0x9001 | ||||||
|     unit_of_measurement: "AH" |     unit_of_measurement: "AH" | ||||||
|     value_type: U_WORD |     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: | switch: | ||||||
|   - platform: modbus_controller |   - platform: modbus_controller | ||||||
| @@ -106,6 +169,16 @@ switch: | |||||||
|     register_type: coil |     register_type: coil | ||||||
|     address: 0x15 |     address: 0x15 | ||||||
|     bitmask: 1 |     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: | text_sensor: | ||||||
|   - platform: modbus_controller |   - platform: modbus_controller | ||||||
| @@ -117,3 +190,13 @@ text_sensor: | |||||||
|     register_count: 3 |     register_count: 3 | ||||||
|     raw_encode: HEXBYTES |     raw_encode: HEXBYTES | ||||||
|     response_size: 6 |     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 glob | ||||||
| import logging | import logging | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
| from esphome import config as config_module, yaml_util | from esphome import config as config_module, yaml_util | ||||||
| from esphome.components import substitutions | 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") |     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): | def test_substitutions_fixtures(fixture_path): | ||||||
|     base_dir = fixture_path / "substitutions" |     base_dir = fixture_path / "substitutions" | ||||||
|     sources = sorted(glob.glob(str(base_dir / "*.input.yaml"))) |     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) |             substitutions.do_substitution_pass(config, None) | ||||||
|  |  | ||||||
|             resolve_extend_remove(config) |             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 |             # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE | ||||||
|             if expected_path.is_file(): |             if expected_path.is_file(): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user