This commit is contained in:
fakuivan 2024-05-02 15:25:52 +12:00 committed by GitHub
commit 04d462fe4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 108 additions and 32 deletions

View File

@ -28,7 +28,7 @@ def validate_substitution_key(value):
CONFIG_SCHEMA = cv.Schema(
{
validate_substitution_key: cv.string_strict,
validate_substitution_key: cv.Any(None, str, int, float, dict, list),
}
)
@ -37,52 +37,87 @@ async def to_code(config):
pass
def _expand_substitutions(substitutions, value, path, ignore_missing):
def _find_tokens(value):
"""
Finds substitutable tokens in the form of:
```
"a variable $a and a variable $abc"
```
and turns them into pairs of:
```
('a', (11, 2)), ('abc', (16, 4))
```
where the first number represents the characters
skipped from the last token to the start of the new one
and the last number the length of the token
(including $ or ${})
"""
if "$" not in value:
return value
return
orig_value = value
i = 0
while True:
m = cv.VARIABLE_PROG.search(value, i)
if not m:
# Nothing more to match. Done
break
i, j = m.span(0)
name = m.group(1)
last_end = 0
for match in cv.VARIABLE_PROG.finditer(value):
name = match.group(1)
start, end = match.span(0)
if name.startswith("{") and name.endswith("}"):
name = name[1:-1]
yield name, (start - last_end, end - start)
last_end = end
def _expand_substitutions(substitutions, value, path, ignore_missing, is_key=False):
substituted = ""
start_from = 0
for name, (ignored_chars, length) in _find_tokens(value):
if name not in substitutions:
if not ignore_missing and "password" not in path:
_LOGGER.warning(
"Found '%s' (see %s) which looks like a substitution, but '%s' was "
"not declared",
orig_value,
value,
"->".join(str(x) for x in path),
name,
)
i = j
substituted += value[start_from : start_from + ignored_chars + length]
start_from += ignored_chars + length
continue
sub = substitutions[name]
tail = value[j:]
value = value[:i] + sub
i = len(value)
value += tail
if isinstance(sub, str):
substituted += value[start_from : start_from + ignored_chars] + sub
start_from += ignored_chars + length
continue
# orig_value can also already be a lambda with esp_range info, and only
# a plain string is sent in orig_value
if isinstance(orig_value, ESPHomeDataBase):
if is_key:
raise cv.Invalid(
"Key substitution is only allowed for string types, "
f"however {name!r} (used in {'->'.join(str(x) for x in path)}) "
f"is of type {type(sub)}"
)
if length != len(value):
raise cv.Invalid(
"String interpolation is only allowed for substitutions with "
f"string types, however {name!r} (used in {'->'.join(str(x) for x in path)}) "
f"is of type {type(sub)}"
)
return sub
substituted += value[start_from:]
# value can also already be a lambda with esp_range info, and only
# a plain string is sent in value
if isinstance(value, ESPHomeDataBase):
# even though string can get larger or smaller, the range should point
# to original document marks
return make_data_base(value, orig_value)
return make_data_base(substituted, value)
return value
return substituted
def _substitute_item(substitutions, item, path, ignore_missing):
def _substitute_item(substitutions, item, path, ignore_missing, is_key=False):
if isinstance(item, list):
for i, it in enumerate(item):
sub = _substitute_item(substitutions, it, path + [i], ignore_missing)
@ -91,8 +126,11 @@ def _substitute_item(substitutions, item, path, ignore_missing):
elif isinstance(item, dict):
replace_keys = []
for k, v in item.items():
# if we're not in the substitutions section, substitute keys
if path or k != CONF_SUBSTITUTIONS:
sub = _substitute_item(substitutions, k, path + [k], ignore_missing)
sub = _substitute_item(
substitutions, k, path + [k], ignore_missing, is_key=True
)
if sub is not None:
replace_keys.append((k, sub))
sub = _substitute_item(substitutions, v, path + [k], ignore_missing)
@ -102,11 +140,13 @@ def _substitute_item(substitutions, item, path, ignore_missing):
item[new] = merge_config(item.get(old), item.get(new))
del item[old]
elif isinstance(item, str):
sub = _expand_substitutions(substitutions, item, path, ignore_missing)
sub = _expand_substitutions(substitutions, item, path, ignore_missing, is_key)
if sub != item:
return sub
elif isinstance(item, core.Lambda):
sub = _expand_substitutions(substitutions, item.value, path, ignore_missing)
sub = _expand_substitutions(
substitutions, item.value, path, ignore_missing, is_key
)
if sub != item:
item.value = sub
return None
@ -133,7 +173,7 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals
sub = validate_substitution_key(key)
if sub != key:
replace_keys.append((key, sub))
substitutions[key] = cv.string_strict(value)
substitutions[key] = value
for old, new in replace_keys:
substitutions[new] = substitutions[old]
del substitutions[old]

View File

@ -279,8 +279,6 @@ class ESPHomeLoaderMixin:
if file is None:
raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark)
vars = fields.get("vars")
if vars:
vars = {k: str(v) for k, v in vars.items()}
return file, vars
def substitute_vars(config, vars):

View File

@ -0,0 +1,5 @@
sensor:
- id: $included_sensor_id
platform: template
name: Inlcuded sensor
- $included_sensor

View File

@ -0,0 +1,33 @@
esphome:
name: test
substitutions:
switch_def:
platform: template
name: $name
optimistic: true
nested_def: $switch_def
double_nested_def: $nested_def
name: Normal switch
valid_interpolation: "1"
included_sensor_name: Included sensor 2
packages:
included: !include
file: included.yaml
vars:
included_sensor_id: included_sensor
included_sensor:
name: $included_sensor_name
id: included_sensor2
platform: template
switch:
- $double_nested_def
- $switch_def
- platform: template
name: $name
optimistic: true
- platform: template
name: Switch ${name} ${valid_interpolation} ${name} ${name}
optimistic: true