mirror of
https://github.com/esphome/esphome.git
synced 2025-09-13 08:42:18 +01:00
Checks for pins used in multiple places (#5666)
This commit is contained in:
185
esphome/pins.py
185
esphome/pins.py
@@ -1,5 +1,7 @@
|
||||
import operator
|
||||
from functools import reduce
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE, ID
|
||||
|
||||
from esphome.const import (
|
||||
CONF_INPUT,
|
||||
@@ -10,16 +12,120 @@ from esphome.const import (
|
||||
CONF_PULLDOWN,
|
||||
CONF_PULLUP,
|
||||
CONF_IGNORE_STRAPPING_WARNING,
|
||||
CONF_ALLOW_OTHER_USES,
|
||||
CONF_INVERTED,
|
||||
)
|
||||
from esphome.util import PinRegistry
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
class PinRegistry(dict):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pins_used = {}
|
||||
|
||||
def reset(self):
|
||||
self.pins_used = {}
|
||||
|
||||
def get_count(self, key, number):
|
||||
"""
|
||||
Get the number of places a given pin is used.
|
||||
:param key: The ID of the defining component
|
||||
:param number: The pin number
|
||||
:return: The number of places the pin is used.
|
||||
"""
|
||||
pin_key = (key, number)
|
||||
return self.pins_used[pin_key] if pin_key in self.pins_used else 0
|
||||
|
||||
def register(self, name, schema, final_validate=None):
|
||||
"""
|
||||
Register a pin schema
|
||||
:param name:
|
||||
:param schema:
|
||||
:param final_validate:
|
||||
:return:
|
||||
"""
|
||||
|
||||
def decorator(fun):
|
||||
self[name] = (fun, schema, final_validate)
|
||||
return fun
|
||||
|
||||
return decorator
|
||||
|
||||
def validate(self, conf, key=None):
|
||||
"""
|
||||
Validate a pin against a registered schema
|
||||
:param conf The pin config
|
||||
:param key: an optional scalar key (e.g. platform)
|
||||
:return: The transformed result
|
||||
"""
|
||||
from esphome.config import path_context
|
||||
|
||||
key = self.get_key(conf) if key is None else key
|
||||
# Element 1 is the pin validation function
|
||||
# evaluate here so a validation failure skips the rest
|
||||
result = self[key][1](conf)
|
||||
if CONF_NUMBER in result:
|
||||
# key maps to the pin schema
|
||||
if isinstance(key, ID):
|
||||
key = key.id
|
||||
pin_key = (key, result[CONF_NUMBER])
|
||||
if pin_key not in self.pins_used:
|
||||
self.pins_used[pin_key] = []
|
||||
# client_id identifies the instance of the providing component
|
||||
client_id = result.get(key)
|
||||
self.pins_used[pin_key].append((path_context.get(), client_id, result))
|
||||
# return the validated pin config
|
||||
return result
|
||||
|
||||
def get_key(self, conf):
|
||||
"""
|
||||
Is there a key in conf corresponding to a registered pin schema?
|
||||
If not, fall back to the default platform schema.
|
||||
:param conf The config for the component
|
||||
:return: the schema key
|
||||
"""
|
||||
keys = list(filter(lambda k: k in conf, self))
|
||||
return keys[0] if keys else CORE.target_platform
|
||||
|
||||
def get_to_code(self, key):
|
||||
"""
|
||||
Return the code generator function for a pin schema, stored as tuple element 0
|
||||
:param conf: The pin config
|
||||
:param key An optional specific key
|
||||
:return: The awaitable coroutine
|
||||
"""
|
||||
key = self.get_key(key) if isinstance(key, dict) else key
|
||||
return self[key][0]
|
||||
|
||||
def final_validate(self, fconf):
|
||||
"""
|
||||
Run the final validation for all pins, and check for reuse
|
||||
:param fconf: The full config
|
||||
"""
|
||||
for (key, _), pin_list in self.pins_used.items():
|
||||
count = len(pin_list) # number of places same pin used.
|
||||
final_val_fun = self[key][2] # final validation function
|
||||
for pin_path, client_id, pin_config in pin_list:
|
||||
with fconf.catch_error([cv.ROOT_CONFIG_PATH] + pin_path):
|
||||
if final_val_fun is not None:
|
||||
# Get the containing path of the config providing this pin.
|
||||
parent_path = fconf.get_path_for_id(client_id)[:-1]
|
||||
parent_config = fconf.get_config_for_path(parent_path)
|
||||
final_val_fun(pin_config, parent_config)
|
||||
allow_others = pin_config.get(CONF_ALLOW_OTHER_USES, False)
|
||||
if count != 1 and not allow_others:
|
||||
raise cv.Invalid(
|
||||
f"Pin {pin_config[CONF_NUMBER]} is used in multiple places"
|
||||
)
|
||||
if count == 1 and allow_others:
|
||||
raise cv.Invalid(
|
||||
f"Pin {pin_config[CONF_NUMBER]} incorrectly sets {CONF_ALLOW_OTHER_USES}: true"
|
||||
)
|
||||
|
||||
|
||||
PIN_SCHEMA_REGISTRY = PinRegistry()
|
||||
|
||||
|
||||
def _set_mode(value, default_mode):
|
||||
import esphome.config_validation as cv
|
||||
|
||||
if CONF_MODE not in value:
|
||||
return {**value, CONF_MODE: default_mode}
|
||||
mode = value[CONF_MODE]
|
||||
@@ -65,20 +171,26 @@ def _schema_creator(default_mode, internal: bool = False):
|
||||
if not isinstance(value, dict):
|
||||
return validator({CONF_NUMBER: value})
|
||||
value = _set_mode(value, default_mode)
|
||||
if not internal:
|
||||
for key, entry in PIN_SCHEMA_REGISTRY.items():
|
||||
if key != CORE.target_platform and key in value:
|
||||
return entry[1](value)
|
||||
return PIN_SCHEMA_REGISTRY[CORE.target_platform][1](value)
|
||||
if internal:
|
||||
return PIN_SCHEMA_REGISTRY.validate(value, CORE.target_platform)
|
||||
return PIN_SCHEMA_REGISTRY.validate(value)
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def _internal_number_creator(mode):
|
||||
def validator(value):
|
||||
value_d = {CONF_NUMBER: value}
|
||||
if isinstance(value, dict):
|
||||
if CONF_MODE in value or CONF_INVERTED in value:
|
||||
raise cv.Invalid(
|
||||
"This variable only supports pin numbers, not full pin schemas "
|
||||
"(with inverted and mode)."
|
||||
)
|
||||
value_d = value
|
||||
else:
|
||||
value_d = {CONF_NUMBER: value}
|
||||
value_d = _set_mode(value_d, mode)
|
||||
return PIN_SCHEMA_REGISTRY[CORE.target_platform][1](value_d)[CONF_NUMBER]
|
||||
return PIN_SCHEMA_REGISTRY.validate(value_d, CORE.target_platform)[CONF_NUMBER]
|
||||
|
||||
return validator
|
||||
|
||||
@@ -149,8 +261,6 @@ internal_gpio_input_pullup_pin_number = _internal_number_creator(
|
||||
|
||||
|
||||
def check_strapping_pin(conf, strapping_pin_list, logger):
|
||||
import esphome.config_validation as cv
|
||||
|
||||
num = conf[CONF_NUMBER]
|
||||
if num in strapping_pin_list and not conf.get(CONF_IGNORE_STRAPPING_WARNING):
|
||||
logger.warning(
|
||||
@@ -161,3 +271,52 @@ def check_strapping_pin(conf, strapping_pin_list, logger):
|
||||
# mitigate undisciplined use of strapping:
|
||||
if num not in strapping_pin_list and conf.get(CONF_IGNORE_STRAPPING_WARNING):
|
||||
raise cv.Invalid(f"GPIO{num} is not a strapping pin")
|
||||
|
||||
|
||||
GPIO_STANDARD_MODES = (
|
||||
CONF_INPUT,
|
||||
CONF_OUTPUT,
|
||||
CONF_OPEN_DRAIN,
|
||||
CONF_PULLUP,
|
||||
CONF_PULLDOWN,
|
||||
)
|
||||
|
||||
|
||||
def gpio_validate_modes(value):
|
||||
if not value[CONF_INPUT] and not value[CONF_OUTPUT]:
|
||||
raise cv.Invalid("Mode must be input or output")
|
||||
return value
|
||||
|
||||
|
||||
def gpio_base_schema(
|
||||
pin_type,
|
||||
number_validator,
|
||||
modes=GPIO_STANDARD_MODES,
|
||||
mode_validator=gpio_validate_modes,
|
||||
invertable=True,
|
||||
):
|
||||
"""
|
||||
Generate a base gpio pin schema
|
||||
:param pin_type: The type for the pin variable
|
||||
:param number_validator: A validator for the pin number
|
||||
:param modes: The available modes, default is all standard modes
|
||||
:param mode_validator: A validator function for the pin mode
|
||||
:param invertable: If the pin supports hardware inversion
|
||||
:return: A schema for the pin
|
||||
"""
|
||||
mode_default = len(modes) == 1
|
||||
mode_dict = dict(
|
||||
map(lambda m: (cv.Optional(m, default=mode_default), cv.boolean), modes)
|
||||
)
|
||||
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(pin_type),
|
||||
cv.Required(CONF_NUMBER): number_validator,
|
||||
cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean,
|
||||
cv.Optional(CONF_MODE, default={}): cv.All(mode_dict, mode_validator),
|
||||
}
|
||||
)
|
||||
if invertable:
|
||||
return schema.extend({cv.Optional(CONF_INVERTED, default=False): cv.boolean})
|
||||
return schema
|
||||
|
Reference in New Issue
Block a user