-
-
Notifications
You must be signed in to change notification settings - Fork 37.7k
Support templating MQTT triggers #45614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
aa3eb1f
f8388b3
2160b0c
106cd75
2fdcf6a
9cf182b
22506d4
1ac04a3
eeac975
0256c00
baa72ce
d185abc
be5908d
5c7a50e
df7598b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |||||
| import voluptuous as vol | ||||||
|
|
||||||
| from homeassistant.const import CONF_PAYLOAD | ||||||
| from homeassistant.helpers import config_validation as cv | ||||||
| from homeassistant.helpers import config_validation as cv, template | ||||||
|
|
||||||
| from .const import ( | ||||||
| ATTR_PAYLOAD, | ||||||
|
|
@@ -61,6 +61,14 @@ def valid_subscribe_topic(value: Any) -> str: | |||||
| return value | ||||||
|
|
||||||
|
|
||||||
| def valid_subscribe_topic_template(value: Any) -> Any: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Is it a good idea to return different types, especially since Template has an optimization if it's static and so has hardly any overhead? |
||||||
| """Validate either a jinja2 template or a valid MQTT subscription topic.""" | ||||||
| if template.is_template_string(value): | ||||||
| return cv.template(value) | ||||||
|
|
||||||
| return valid_subscribe_topic(value) | ||||||
|
|
||||||
|
|
||||||
| def valid_publish_topic(value: Any) -> str: | ||||||
| """Validate that we can publish using this MQTT topic.""" | ||||||
| value = valid_topic(value) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,7 +84,9 @@ def attach(hass: HomeAssistantType, obj: Any) -> None: | |
| obj.hass = hass | ||
|
|
||
|
|
||
| def render_complex(value: Any, variables: TemplateVarsType = None) -> Any: | ||
| def render_complex( | ||
| value: Any, variables: TemplateVarsType = None, limited: bool = False | ||
| ) -> Any: | ||
| """Recursive template creator helper function.""" | ||
| if isinstance(value, list): | ||
| return [render_complex(item, variables) for item in value] | ||
|
|
@@ -94,7 +96,7 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any: | |
| for key, item in value.items() | ||
| } | ||
| if isinstance(value, Template): | ||
| return value.async_render(variables) | ||
| return value.async_render(variables, limited=limited) | ||
|
|
||
| return value | ||
|
|
||
|
|
@@ -279,6 +281,7 @@ class Template: | |
| "is_static", | ||
| "_compiled_code", | ||
| "_compiled", | ||
| "_limited", | ||
| ) | ||
|
|
||
| def __init__(self, template, hass=None): | ||
|
|
@@ -291,10 +294,11 @@ def __init__(self, template, hass=None): | |
| self._compiled: Optional[Template] = None | ||
| self.hass = hass | ||
| self.is_static = not is_template_string(template) | ||
| self._limited = None | ||
|
|
||
| @property | ||
| def _env(self) -> "TemplateEnvironment": | ||
| if self.hass is None: | ||
| if self.hass is None or self._limited: | ||
| return _NO_HASS_ENV | ||
| ret: Optional[TemplateEnvironment] = self.hass.data.get(_ENVIRONMENT) | ||
| if ret is None: | ||
|
|
@@ -315,36 +319,43 @@ def render( | |
| self, | ||
| variables: TemplateVarsType = None, | ||
| parse_result: bool = True, | ||
| limited: bool = False, | ||
| **kwargs: Any, | ||
| ) -> Any: | ||
| """Render given template.""" | ||
| """Render given template. | ||
|
|
||
| If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. | ||
| """ | ||
| if self.is_static: | ||
| if self.hass.config.legacy_templates or not parse_result: | ||
| return self.template | ||
| return self._parse_result(self.template) | ||
|
|
||
| return run_callback_threadsafe( | ||
| self.hass.loop, | ||
| partial(self.async_render, variables, parse_result, **kwargs), | ||
| partial(self.async_render, variables, parse_result, limited, **kwargs), | ||
| ).result() | ||
|
|
||
| @callback | ||
| def async_render( | ||
| self, | ||
| variables: TemplateVarsType = None, | ||
| parse_result: bool = True, | ||
| limited: bool = False, | ||
|
balloob marked this conversation as resolved.
|
||
| **kwargs: Any, | ||
| ) -> Any: | ||
| """Render given template. | ||
|
|
||
| This method must be run in the event loop. | ||
|
|
||
| If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's nice to break long strings around 88 characters. |
||
| """ | ||
| if self.is_static: | ||
| if self.hass.config.legacy_templates or not parse_result: | ||
| return self.template | ||
| return self._parse_result(self.template) | ||
|
|
||
| compiled = self._compiled or self._ensure_compiled() | ||
| compiled = self._compiled or self._ensure_compiled(limited) | ||
|
|
||
| if variables is not None: | ||
| kwargs.update(variables) | ||
|
|
@@ -519,12 +530,16 @@ def async_render_with_possible_json_value( | |
| ) | ||
| return value if error_value is _SENTINEL else error_value | ||
|
|
||
| def _ensure_compiled(self) -> "Template": | ||
| def _ensure_compiled(self, limited: bool = False) -> "Template": | ||
| """Bind a template to a specific hass instance.""" | ||
| self.ensure_valid() | ||
|
|
||
| assert self.hass is not None, "hass variable not set on template" | ||
| assert ( | ||
| self._limited is None or self._limited == limited | ||
| ), "can't change between limited and non limited template" | ||
|
|
||
| self._limited = limited | ||
| env = self._env | ||
|
|
||
| self._compiled = cast( | ||
|
|
@@ -1352,6 +1367,31 @@ def __init__(self, hass): | |
| self.globals["strptime"] = strptime | ||
| self.globals["urlencode"] = urlencode | ||
| if hass is None: | ||
|
|
||
| def unsupported(name): | ||
| def warn_unsupported(*args, **kwargs): | ||
| raise TemplateError( | ||
| f"Use of '{name}' is not supported in limited templates" | ||
| ) | ||
|
|
||
| return warn_unsupported | ||
|
|
||
| hass_globals = [ | ||
| "closest", | ||
| "distance", | ||
| "expand", | ||
| "is_state", | ||
| "is_state_attr", | ||
| "state_attr", | ||
| "states", | ||
| "utcnow", | ||
| "now", | ||
| ] | ||
| hass_filters = ["closest", "expand"] | ||
| for glob in hass_globals: | ||
| self.globals[glob] = unsupported(glob) | ||
| for filt in hass_filters: | ||
| self.filters[filt] = unsupported(filt) | ||
| return | ||
|
|
||
| # We mark these as a context functions to ensure they get | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.