diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py new file mode 100644 index 0000000000..729af4a9a0 --- /dev/null +++ b/esphome/components/packages/__init__.py @@ -0,0 +1,51 @@ +from deepmerge import conservative_merger as package_merger + +import esphome.config_validation as cv + +from esphome.const import CONF_PACKAGES + +VALID_PACKAGE_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_' + + +def _merge_package(config, package_name, package_config): + config = config.copy() + package_merger.merge(config, package_config) + return config + + +def _is_valid_package_name(value: str) -> bool: + if not value: + return False + if value[0].isdigit(): + return False + try: + cv.valid_name(value) + except cv.Invalid: + return False + return True + + +def do_packages_pass(config: dict): + if CONF_PACKAGES not in config: + return + packages = config[CONF_PACKAGES] + temp_config = config.copy() + with cv.prepend_path(CONF_PACKAGES): + if packages is not None and not isinstance(packages, dict): + raise cv.Invalid("Packages must be a key to value mapping, got {} instead" + "".format(type(packages))) + for package_name, package_config in packages.items(): + with cv.prepend_path(package_name): + if not isinstance(package_config, dict): + raise cv.Invalid("Package definition must be a dictionary containing valid " + "esphome configuration to be merged with the main " + "config, got {} instead" + .format(type(package_config))) + if not _is_valid_package_name(package_name): + raise cv.Invalid("Package name is invalid. Valid name should consist of " + "letters, numbers and underscores. It shouldn't also " + "start with number") + temp_config = _merge_package(temp_config, package_name, package_config) + del temp_config[CONF_PACKAGES] + config.clear() + config.update(temp_config) diff --git a/esphome/config.py b/esphome/config.py index 741b4fb04a..6f3a498444 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -12,8 +12,9 @@ import voluptuous as vol from esphome import core, core_config, yaml_util from esphome.components import substitutions +from esphome.components.packages import do_packages_pass from esphome.components.substitutions import CONF_SUBSTITUTIONS -from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS +from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS, CONF_PACKAGES from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent from esphome.util import safe_print, OrderedDict @@ -390,6 +391,16 @@ def recursive_check_replaceme(value): def validate_config(config, command_line_substitutions): result = Config() + # 0. Load packages + if CONF_PACKAGES in config: + result.add_output_path([CONF_PACKAGES], CONF_PACKAGES) + try: + do_packages_pass(config) + except vol.Invalid as err: + result.update(config) + result.add_error(err) + return result + # 1. Load substitutions if CONF_SUBSTITUTIONS in config: result[CONF_SUBSTITUTIONS] = {**config[CONF_SUBSTITUTIONS], **command_line_substitutions} diff --git a/esphome/const.py b/esphome/const.py index 1760db0dab..45b5e5e209 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -342,6 +342,7 @@ CONF_OUTPUT = 'output' CONF_OUTPUT_ID = 'output_id' CONF_OUTPUTS = 'outputs' CONF_OVERSAMPLING = 'oversampling' +CONF_PACKAGES = 'packages' CONF_PAGE_ID = 'page_id' CONF_PAGES = 'pages' CONF_PANASONIC = 'panasonic' diff --git a/requirements.txt b/requirements.txt index 0c99eaccbe..0a3573f7b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ ifaddr==0.1.6 platformio==4.3.3 esptool==2.8 click==7.1.2 +deepmerge==0.1.0 diff --git a/requirements_test.txt b/requirements_test.txt index b90c4ea969..da4656b085 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,6 +10,7 @@ pyserial==3.4 ifaddr==0.1.6 platformio==4.3.3 esptool==2.8 +deepmerge==0.1.0 pylint==2.5.0 flake8==3.7.9 diff --git a/setup.py b/setup.py index 0a19dccf95..ede80b34e1 100755 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ REQUIRES = [ 'pytz==2020.1', 'pyserial==3.4', 'ifaddr==0.1.6', + 'deepmerge==0.1.0' ] # If you have problems importing platformio and esptool as modules you can set diff --git a/tests/test1.yaml b/tests/test1.yaml index 4a47ffd7b9..b131b1a6ab 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1,3 +1,6 @@ +substitutions: + devicename: test1 + esphome: name: test1 platform: ESP32 @@ -25,6 +28,10 @@ esphome: white: 100% build_path: build/test1 +packages: + wifi: !include test_packages/test_packages_package_wifi.yaml + pkg_test: !include test_packages/test_packages_package1.yaml + wifi: networks: - ssid: 'MySSID' diff --git a/tests/test_packages/test_packages_package1.yaml b/tests/test_packages/test_packages_package1.yaml new file mode 100644 index 0000000000..0495984d42 --- /dev/null +++ b/tests/test_packages/test_packages_package1.yaml @@ -0,0 +1,2 @@ +sensor: + - <<: !include ./test_uptime_sensor.yaml diff --git a/tests/test_packages/test_packages_package_wifi.yaml b/tests/test_packages/test_packages_package_wifi.yaml new file mode 100644 index 0000000000..7d5d41ddab --- /dev/null +++ b/tests/test_packages/test_packages_package_wifi.yaml @@ -0,0 +1,4 @@ +wifi: + networks: + - ssid: 'WiFiFromPackage' + password: 'password1' diff --git a/tests/test_packages/test_uptime_sensor.yaml b/tests/test_packages/test_uptime_sensor.yaml new file mode 100644 index 0000000000..1bf52a6d0b --- /dev/null +++ b/tests/test_packages/test_uptime_sensor.yaml @@ -0,0 +1,5 @@ +# Uptime sensor. +platform: uptime +id: ${devicename}_uptime_pcg +name: Uptime From Package +update_interval: 5min