mirror of https://github.com/esphome/esphome.git
Merge 651e973200
into c7c0d97a5e
This commit is contained in:
commit
52100291e9
|
@ -124,6 +124,7 @@ esphome/components/esp8266/* @esphome/core
|
|||
esphome/components/ethernet_info/* @gtjadsonsantos
|
||||
esphome/components/event/* @nohat
|
||||
esphome/components/exposure_notifications/* @OttoWinter
|
||||
esphome/components/extensions/* @kpfleming
|
||||
esphome/components/ezo/* @ssieb
|
||||
esphome/components/ezo_pmp/* @carlos-sarmiento
|
||||
esphome/components/factory_reset/* @anatoly-savchenkov
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
from typing import Any, Union
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_helpers import merge_config
|
||||
from esphome.const import (
|
||||
CONF_EXTENSIONS,
|
||||
CONF_ID,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@kpfleming"]
|
||||
|
||||
DOMAIN = CONF_EXTENSIONS
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.validate_id_name: dict,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ObjectInDict:
|
||||
def __init__(self, dic: dict, key: str):
|
||||
self.dic = dic
|
||||
self.key = key
|
||||
|
||||
|
||||
class ObjectInList:
|
||||
def __init__(self, lis: list, index: int):
|
||||
self.lis = lis
|
||||
self.index = index
|
||||
|
||||
|
||||
def gather_object_ids(config: dict) -> dict[str, Union[ObjectInDict, ObjectInList]]:
|
||||
result = {}
|
||||
for k, v in config.items():
|
||||
if k == CONF_EXTENSIONS:
|
||||
continue
|
||||
if isinstance(v, dict):
|
||||
if CONF_ID in v and isinstance(v[CONF_ID], str):
|
||||
result[v[CONF_ID]] = ObjectInDict(config, k)
|
||||
result.update(gather_object_ids(v))
|
||||
elif isinstance(v, list):
|
||||
for i, o in enumerate(v):
|
||||
if isinstance(o, dict):
|
||||
if CONF_ID in o and isinstance(o[CONF_ID], str):
|
||||
result[o[CONF_ID]] = ObjectInList(v, i)
|
||||
result.update(gather_object_ids(o))
|
||||
return result
|
||||
|
||||
|
||||
def do_extensions_pass(config: dict) -> dict[str, Any]:
|
||||
if CONF_EXTENSIONS not in config:
|
||||
return config
|
||||
extensions = config[CONF_EXTENSIONS]
|
||||
with cv.prepend_path(CONF_EXTENSIONS):
|
||||
extensions = CONFIG_SCHEMA(extensions)
|
||||
|
||||
while extensions:
|
||||
matched = False
|
||||
ids = gather_object_ids(config)
|
||||
|
||||
for id in [id for id in extensions.keys() if id in ids]:
|
||||
matched = True
|
||||
obj = ids[id]
|
||||
if isinstance(obj, ObjectInDict):
|
||||
obj.dic[obj.key] = merge_config(obj.dic[obj.key], extensions[id])
|
||||
elif isinstance(obj, ObjectInList):
|
||||
obj.lis[obj.index] = merge_config(
|
||||
obj.lis[obj.index], extensions[id]
|
||||
)
|
||||
del extensions[id]
|
||||
|
||||
if not matched:
|
||||
break
|
||||
|
||||
# if any extension IDs were not found, report them
|
||||
if extensions:
|
||||
raise cv.MultipleInvalid(
|
||||
[
|
||||
cv.Invalid(f"No match for extension ID {k} found in configuration.")
|
||||
for k in extensions.keys()
|
||||
]
|
||||
)
|
||||
|
||||
del config[CONF_EXTENSIONS]
|
||||
return config
|
|
@ -16,6 +16,7 @@ from esphome import core, yaml_util, loader, pins
|
|||
import esphome.core.config as core_config
|
||||
from esphome.const import (
|
||||
CONF_ESPHOME,
|
||||
CONF_EXTENSIONS,
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_PACKAGES,
|
||||
|
@ -801,6 +802,8 @@ def validate_config(
|
|||
except vol.Invalid as err:
|
||||
result.add_error(err)
|
||||
|
||||
CORE.raw_config = config
|
||||
|
||||
# 1.2. Load external_components
|
||||
if CONF_EXTERNAL_COMPONENTS in config:
|
||||
from esphome.components.external_components import do_external_components_pass
|
||||
|
@ -813,6 +816,20 @@ def validate_config(
|
|||
result.add_error(err)
|
||||
return result
|
||||
|
||||
# 2. Process object extensions
|
||||
if CONF_EXTENSIONS in config:
|
||||
from esphome.components.extensions import do_extensions_pass
|
||||
|
||||
result.add_output_path([CONF_EXTENSIONS], CONF_EXTENSIONS)
|
||||
try:
|
||||
config = do_extensions_pass(config)
|
||||
except vol.Invalid as err:
|
||||
result.update(config)
|
||||
result.add_error(err)
|
||||
return result
|
||||
|
||||
CORE.raw_config = config
|
||||
|
||||
if "esphomeyaml" in config:
|
||||
_LOGGER.warning(
|
||||
"The esphomeyaml section has been renamed to esphome in 1.11.0. "
|
||||
|
|
|
@ -257,6 +257,7 @@ CONF_EVENT_TYPES = "event_types"
|
|||
CONF_EXPIRE_AFTER = "expire_after"
|
||||
CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy"
|
||||
CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy"
|
||||
CONF_EXTENSIONS = "extensions"
|
||||
CONF_EXTERNAL_CLOCK_INPUT = "external_clock_input"
|
||||
CONF_EXTERNAL_COMPONENTS = "external_components"
|
||||
CONF_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
"""Tests for the extensions component."""
|
||||
|
||||
import pytest
|
||||
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from esphome.const import (
|
||||
CONF_EXTENSIONS,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_SENSOR,
|
||||
)
|
||||
from esphome.components.extensions import do_extensions_pass
|
||||
|
||||
# Test strings
|
||||
TEST_OBJECT_NAME_1 = "test_object_name_1"
|
||||
TEST_SENSOR_NAME_1 = "test_sensor_name_1"
|
||||
TEST_SENSOR_NAME_2 = "test_sensor_name_2"
|
||||
TEST_SENSOR_ID_1 = "test_sensor_id_1"
|
||||
TEST_SENSOR_ID_2 = "test_sensor_id_2"
|
||||
|
||||
|
||||
def test_basic():
|
||||
"""
|
||||
Ensure that the name provided by the extension is added to the object.
|
||||
"""
|
||||
config = {
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_ID: TEST_SENSOR_ID_1,
|
||||
}
|
||||
],
|
||||
CONF_EXTENSIONS: {
|
||||
TEST_SENSOR_ID_1: {
|
||||
CONF_NAME: TEST_SENSOR_NAME_1,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
expected = {
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_ID: TEST_SENSOR_ID_1,
|
||||
CONF_NAME: TEST_SENSOR_NAME_1,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
actual = do_extensions_pass(config)
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_basic_override():
|
||||
"""
|
||||
Ensure that the name provided by the extension overrides the one in the object.
|
||||
"""
|
||||
config = {
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_ID: TEST_SENSOR_ID_1,
|
||||
CONF_NAME: TEST_SENSOR_NAME_1,
|
||||
}
|
||||
],
|
||||
CONF_EXTENSIONS: {
|
||||
TEST_SENSOR_ID_1: {
|
||||
CONF_NAME: TEST_SENSOR_NAME_2,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
expected = {
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_ID: TEST_SENSOR_ID_1,
|
||||
CONF_NAME: TEST_SENSOR_NAME_2,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
actual = do_extensions_pass(config)
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_embedded():
|
||||
"""
|
||||
Ensure that the name provided by the extension is added to the embedded object.
|
||||
"""
|
||||
config = {
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
TEST_OBJECT_NAME_1: {
|
||||
CONF_ID: TEST_SENSOR_ID_1,
|
||||
},
|
||||
}
|
||||
],
|
||||
CONF_EXTENSIONS: {
|
||||
TEST_SENSOR_ID_1: {
|
||||
CONF_NAME: TEST_SENSOR_NAME_1,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
expected = {
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
TEST_OBJECT_NAME_1: {
|
||||
CONF_ID: TEST_SENSOR_ID_1,
|
||||
CONF_NAME: TEST_SENSOR_NAME_1,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
actual = do_extensions_pass(config)
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_two_pass_embedded():
|
||||
"""
|
||||
Ensure that the name provided by an extension is added to the object added by a previous extension.
|
||||
"""
|
||||
config = {
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_ID: TEST_SENSOR_ID_1,
|
||||
}
|
||||
],
|
||||
CONF_EXTENSIONS: {
|
||||
TEST_SENSOR_ID_1: {
|
||||
TEST_OBJECT_NAME_1: {
|
||||
CONF_ID: TEST_SENSOR_ID_2,
|
||||
},
|
||||
},
|
||||
TEST_SENSOR_ID_2: {
|
||||
CONF_NAME: TEST_SENSOR_NAME_2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected = {
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_ID: TEST_SENSOR_ID_1,
|
||||
TEST_OBJECT_NAME_1: {
|
||||
CONF_ID: TEST_SENSOR_ID_2,
|
||||
CONF_NAME: TEST_SENSOR_NAME_2,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
actual = do_extensions_pass(config)
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_not_found():
|
||||
"""
|
||||
Ensure that a non-matching ID is reported.
|
||||
"""
|
||||
config = {
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_ID: TEST_SENSOR_ID_1,
|
||||
}
|
||||
],
|
||||
CONF_EXTENSIONS: {
|
||||
TEST_SENSOR_ID_2: {
|
||||
CONF_NAME: TEST_SENSOR_NAME_1,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
with pytest.raises(cv.MultipleInvalid, match=f"extension ID {TEST_SENSOR_ID_2}"):
|
||||
do_extensions_pass(config)
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
substitutions:
|
||||
devicename: extensions
|
||||
sensorname: my
|
||||
textname: template
|
||||
roomname: living_room
|
||||
|
||||
esphome:
|
||||
name: extensions
|
||||
platform: ESP32
|
||||
board: nodemcu-32s
|
||||
build_path: build/extensions
|
||||
|
||||
i2c:
|
||||
sda:
|
||||
number: 21
|
||||
scl:
|
||||
number: 22
|
||||
id: i2c_bus
|
||||
|
||||
sensor:
|
||||
- id: sensor_1
|
||||
platform: template
|
||||
|
||||
- id: ${roomname}
|
||||
platform: template
|
||||
|
||||
- <<: !include test_packages/test_uptime_sensor.yaml
|
||||
|
||||
- platform: bmp280
|
||||
temperature:
|
||||
id: temperature
|
||||
name: Outside Temperature
|
||||
pressure:
|
||||
name: Outside Pressure
|
||||
address: 0x77
|
||||
i2c_id: i2c_bus
|
||||
|
||||
extensions:
|
||||
sensor_1:
|
||||
name: ${sensorname}
|
||||
${roomname}:
|
||||
name: sensor_name
|
||||
${devicename}_uptime_pcg:
|
||||
update_interval: 10min
|
||||
temperature:
|
||||
oversampling: 8x
|
Loading…
Reference in New Issue