diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index 3e136dae49..ca829cb274 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -59,3 +59,9 @@ async def to_code(config): conf = config[CONF_POWER] sens = await sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) + + +def validate(config, item_config): + uart.validate_device( + "cse7766", config, item_config, baud_rate=4800, require_tx=False + ) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index d23e40000e..4c276db63f 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -78,6 +78,12 @@ async def to_code(config): await automation.build_automation(trigger, [], conf) +def validate(config, item_config): + uart.validate_device( + "dfplayer", config, item_config, baud_rate=9600, require_rx=False + ) + + @automation.register_action( "dfplayer.play_next", NextAction, diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index c6f81e5046..94c9ddd2e9 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -59,7 +59,7 @@ IPAddress = cg.global_ns.class_("IPAddress") ManualIP = ethernet_ns.struct("ManualIP") -def validate(config): +def _validate(config): if CONF_USE_ADDRESS not in config: if CONF_MANUAL_IP in config: use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP]) @@ -90,7 +90,7 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - validate, + _validate, ) diff --git a/esphome/components/fastled_clockless/light.py b/esphome/components/fastled_clockless/light.py index 17f72427e4..cfc62e930b 100644 --- a/esphome/components/fastled_clockless/light.py +++ b/esphome/components/fastled_clockless/light.py @@ -34,7 +34,7 @@ CHIPSETS = [ ] -def validate(value): +def _validate(value): if value[CONF_CHIPSET] == "NEOPIXEL" and CONF_RGB_ORDER in value: raise cv.Invalid("NEOPIXEL doesn't support RGB order") return value @@ -47,7 +47,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): pins.output_pin, } ), - validate, + _validate, ) diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index e982bd4dfd..57ae11ea4f 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -89,3 +89,7 @@ async def to_code(config): # https://platformio.org/lib/show/1655/TinyGPSPlus cg.add_library("1655", "1.0.2") # TinyGPSPlus, has name conflict + + +def validate(config, item_config): + uart.validate_device("gps", config, item_config, require_tx=False) diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 6d77841d20..34555fdcd6 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -29,7 +29,7 @@ AQI_CALCULATION_TYPE = { } -def validate(config): +def _validate(config): if CONF_AQI in config and CONF_PM_2_5 not in config: raise cv.Invalid("AQI sensor requires PM 2.5") if CONF_AQI in config and CONF_PM_10_0 not in config: @@ -72,7 +72,7 @@ CONFIG_SCHEMA = cv.All( ) .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x40)), - validate, + _validate, ) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index f643b476e8..59d784a614 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -150,7 +150,7 @@ def format_method(config): raise NotImplementedError -def validate(config): +def _validate(config): if CONF_PIN in config: if CONF_CLOCK_PIN in config or CONF_DATA_PIN in config: raise cv.Invalid("Cannot specify both 'pin' and 'clock_pin'+'data_pin'") @@ -176,7 +176,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, } ).extend(cv.COMPONENT_SCHEMA), - validate, + _validate, validate_method_pin, ) diff --git a/esphome/components/rc522_spi/__init__.py b/esphome/components/rc522_spi/__init__.py index b21af1d07f..68b1e64145 100644 --- a/esphome/components/rc522_spi/__init__.py +++ b/esphome/components/rc522_spi/__init__.py @@ -24,3 +24,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await rc522.setup_rc522(var, config) await spi.register_spi_device(var, config) + + +def validate(config, item_config): + # validate given SPI hub is suitable for rc522_spi, it needs both miso and mosi + spi.validate_device("rc522_spi", config, item_config, True, True) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 3441a63b9a..7f860fe3d7 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -1,8 +1,11 @@ +import logging import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components.output import FloatOutput -from esphome.const import CONF_ID, CONF_OUTPUT, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_TRIGGER_ID + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@glmnet"] CONF_RTTTL = "rtttl" @@ -33,6 +36,33 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +def validate(config, item_config): + # Not adding this to FloatOutput as this is the only component which needs `update_frequency` + + parent_config = config.get_config_by_id(item_config[CONF_OUTPUT]) + platform = parent_config[CONF_PLATFORM] + + PWM_GOOD = ["esp8266_pwm", "ledc"] + PWM_BAD = [ + "ac_dimmer ", + "esp32_dac", + "slow_pwm", + "mcp4725", + "pca9685", + "tlc59208f", + "my9231", + "sm16716", + ] + + if platform in PWM_BAD: + raise ValueError(f"Component rtttl cannot use {platform} as output component") + + if platform not in PWM_GOOD: + _LOGGER.warning( + "Component rtttl is not known to work with the selected output type. Make sure this output supports custom frequency output method." + ) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index f9f6d71cd2..40c011a769 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -54,6 +54,10 @@ async def to_code(config): ) +def validate(config, item_config): + uart.validate_device("sim800l", config, item_config, baud_rate=9600) + + SIM800L_SEND_SMS_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(Sim800LComponent), diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 00d21e619e..e6e073c4a4 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -67,3 +67,11 @@ async def register_spi_device(var, config): if CONF_CS_PIN in config: pin = await cg.gpio_pin_expression(config[CONF_CS_PIN]) cg.add(var.set_cs_pin(pin)) + + +def validate_device(name, config, item_config, require_mosi, require_miso): + spi_config = config.get_config_by_id(item_config[CONF_SPI_ID]) + if require_mosi and CONF_MISO_PIN not in spi_config: + raise ValueError(f"Component {name} requires parent spi to declare miso_pin") + if require_miso and CONF_MOSI_PIN not in spi_config: + raise ValueError(f"Component {name} requires parent spi to declare mosi_pin") diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index fb82fd9aee..aaed333e34 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -92,6 +92,42 @@ async def to_code(config): cg.add(var.set_parity(config[CONF_PARITY])) +def validate_device( + name, config, item_config, baud_rate=None, require_tx=True, require_rx=True +): + if not hasattr(config, "uart_devices"): + config.uart_devices = {} + devices = config.uart_devices + + uart_config = config.get_config_by_id(item_config[CONF_UART_ID]) + + uart_id = uart_config[CONF_ID] + device = devices.setdefault(uart_id, {}) + + if require_tx: + if CONF_TX_PIN not in uart_config: + raise ValueError(f"Component {name} requires parent uart to declare tx_pin") + if CONF_TX_PIN in device: + raise ValueError( + f"Component {name} cannot use the same uart.{CONF_TX_PIN} as component {device[CONF_TX_PIN]} is already using it" + ) + device[CONF_TX_PIN] = name + + if require_rx: + if CONF_RX_PIN not in uart_config: + raise ValueError(f"Component {name} requires parent uart to declare rx_pin") + if CONF_RX_PIN in device: + raise ValueError( + f"Component {name} cannot use the same uart.{CONF_RX_PIN} as component {device[CONF_RX_PIN]} is already using it" + ) + device[CONF_RX_PIN] = name + + if baud_rate and uart_config[CONF_BAUD_RATE] != baud_rate: + raise ValueError( + f"Component {name} requires parent uart baud rate be {baud_rate}" + ) + + # A schema to use for all UART devices, all UART integrations must extend this! UART_DEVICE_SCHEMA = cv.Schema( { diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index d2676bce36..c45e179bc4 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -137,7 +137,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( ) -def validate(config): +def _validate(config): if CONF_PASSWORD in config and CONF_SSID not in config: raise cv.Invalid("Cannot have WiFi password without SSID!") @@ -207,7 +207,7 @@ CONFIG_SCHEMA = cv.All( ), } ), - validate, + _validate, ) diff --git a/esphome/config.py b/esphome/config.py index a1fc07a21f..fcd2fac90f 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -63,6 +63,8 @@ class Config(OrderedDict): # The values will be the paths to all "domain", for example (['logger'], 'logger') # or (['sensor', 'ultrasonic'], 'sensor.ultrasonic') self.output_paths = [] # type: List[Tuple[ConfigPath, str]] + # A list of components ids with the config path + self.declare_ids = [] # type: List[Tuple[core.ID, ConfigPath]] def add_error(self, error): # type: (vol.Invalid) -> None @@ -161,6 +163,12 @@ class Config(OrderedDict): part.append(item_index) return part + def get_config_by_id(self, id): + for declared_id, path in self.declare_ids: + if declared_id.id == str(id): + return self.get_nested_item(path[:-1]) + return None + def iter_ids(config, path=None): path = path or [] @@ -181,7 +189,7 @@ def do_id_pass(result): # type: (Config) -> None from esphome.cpp_generator import MockObjClass from esphome.cpp_types import Component - declare_ids = [] # type: List[Tuple[core.ID, ConfigPath]] + declare_ids = result.declare_ids # type: List[Tuple[core.ID, ConfigPath]] searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]] for id, path in iter_ids(result): if id.is_declaration: @@ -546,6 +554,19 @@ def validate_config(config, command_line_substitutions): # Only parse IDs if no validation error. Otherwise # user gets confusing messages do_id_pass(result) + + # 7. Final validation + if not result.errors: + # Inter - components validation + for path, conf, comp in validate_queue: + if comp.config_schema is None: + continue + if callable(comp.validate): + try: + comp.validate(result, result.get_nested_item(path)) + except ValueError as err: + result.add_str_error(err, path) + return result diff --git a/esphome/loader.py b/esphome/loader.py index c418008453..d9d407d787 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -80,6 +80,10 @@ class ComponentManifest: def codeowners(self) -> List[str]: return getattr(self.module, "CODEOWNERS", []) + @property + def validate(self): + return getattr(self.module, "validate", None) + @property def source_files(self) -> Dict[Path, SourceFile]: ret = {} diff --git a/tests/test3.yaml b/tests/test3.yaml index 8104c70fbf..0a01405516 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1,6 +1,6 @@ esphome: - name: $devicename - comment: $devicecomment + name: $device_name + comment: $device_comment platform: ESP8266 board: d1_mini build_path: build/test3 @@ -13,8 +13,8 @@ esphome: - custom.h substitutions: - devicename: test3 - devicecomment: test3 device + device_name: test3 + device_comment: test3 device min_sub: '0.03' max_sub: '12.0%' @@ -213,9 +213,33 @@ spi: miso_pin: GPIO14 uart: - - tx_pin: GPIO1 + - id: uart1 + tx_pin: GPIO1 rx_pin: GPIO3 baud_rate: 115200 + - id: uart2 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 + - id: uart3 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 4800 + - id: uart4 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 + - id: uart5 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 + - id: uart6 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 + +modbus: + uart_id: uart1 ota: safe_mode: True @@ -369,6 +393,7 @@ sensor: active_power_b: name: ADE7953 Active Power B - platform: pzem004t + uart_id: uart3 voltage: name: 'PZEM00T Voltage' current: @@ -408,6 +433,7 @@ sensor: name: 'AQI' calculation_type: 'AQI' - platform: pmsx003 + uart_id: uart2 type: PMSX003 pm_1_0: name: 'PM 1.0 Concentration' @@ -415,25 +441,8 @@ sensor: name: 'PM 2.5 Concentration' pm_10_0: name: 'PM 10.0 Concentration' - - platform: pmsx003 - type: PMS5003T - pm_2_5: - name: 'PM 2.5 Concentration' - temperature: - name: 'PMS Temperature' - humidity: - name: 'PMS Humidity' - - platform: pmsx003 - type: PMS5003ST - pm_2_5: - name: 'PM 2.5 Concentration' - temperature: - name: 'PMS Temperature' - humidity: - name: 'PMS Humidity' - formaldehyde: - name: 'PMS Formaldehyde Concentration' - platform: cse7766 + uart_id: uart3 voltage: name: 'CSE7766 Voltage' current: @@ -443,7 +452,7 @@ sensor: - platform: ezo id: ph_ezo address: 99 - unit_of_measurement: 'pH' + unit_of_measurement: 'pH' - platform: tof10120 name: "Distance sensor" update_interval: 5s @@ -867,6 +876,7 @@ light: effects: - wled: - adalight: + uart_id: uart3 - e131: universe: 1 - platform: hbridge @@ -888,6 +898,7 @@ ttp229_bsf: scl_pin: D1 sim800l: + uart_id: uart4 on_sms_received: - lambda: |- std::string str; @@ -900,6 +911,7 @@ sim800l: recipient: '+1234' dfplayer: + uart_id: uart5 on_finished_playback: then: if: @@ -913,6 +925,7 @@ tm1651: dio_pin: D5 rf_bridge: + uart_id: uart5 on_code_received: - lambda: |- uint32_t test; @@ -1006,3 +1019,4 @@ fingerprint_grow: event: esphome.${devicename}_fingerprint_grow_enrollment_failed data: finger_id: !lambda 'return finger_id;' + uart_id: uart6