mirror of
https://github.com/esphome/esphome.git
synced 2026-10-23 14:38:52 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4db3f0407d | ||
|
|
7e226093a5 | ||
|
|
3956a5b7ad |
No files matched your search
@@ -1,13 +1,37 @@
|
||||
"""Pin checks shared by the ESP32 variant validators."""
|
||||
|
||||
from collections.abc import Iterable, Mapping, Set as AbstractSet
|
||||
import logging
|
||||
from logging import Logger
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
from esphome.const import CONF_NUMBER
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_OUTPUT,
|
||||
CONF_PULLDOWN,
|
||||
CONF_PULLUP,
|
||||
)
|
||||
from esphome.pins import check_strapping_pin
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_NO_PINS: Mapping[int, str] = MappingProxyType({})
|
||||
|
||||
_INPUT_ONLY_REJECTED_MODES = (
|
||||
(CONF_OUTPUT, "does not support output pin mode."),
|
||||
(CONF_PULLUP, "does not support pullups."),
|
||||
(CONF_PULLDOWN, "does not support pulldowns."),
|
||||
)
|
||||
|
||||
|
||||
def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> None:
|
||||
def check_usb_jtag_pin(
|
||||
num: int, usb_jtag_pins: AbstractSet[int], logger: Logger
|
||||
) -> None:
|
||||
if num in usb_jtag_pins:
|
||||
logger.warning(
|
||||
"GPIO%d is used by the USB-Serial-JTAG interface."
|
||||
@@ -17,10 +41,104 @@ def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> Non
|
||||
|
||||
|
||||
def check_usb_jtag_hold(
|
||||
conf: dict[str, Any], usb_jtag_pins: set[int], logger: Logger
|
||||
conf: dict[str, Any], usb_jtag_pins: AbstractSet[int], logger: Logger
|
||||
) -> None:
|
||||
num = conf[CONF_NUMBER]
|
||||
if conf.get(CONF_HOLD_STATE) and num in usb_jtag_pins:
|
||||
logger.warning(
|
||||
"GPIO%d cannot hold at low level during wakeup from deep sleep.", num
|
||||
)
|
||||
|
||||
|
||||
def spi_psram_errors(variant: str, functions: Mapping[int, str]) -> dict[int, str]:
|
||||
"""Error messages for pins taken by the SPI flash/PSRAM interface."""
|
||||
return {
|
||||
num: f"This pin cannot be used on {variant}s and is already used by the"
|
||||
f" SPI/PSRAM interface (function: {function})"
|
||||
for num, function in functions.items()
|
||||
}
|
||||
|
||||
|
||||
def unusable_errors(variant: str, pins: Iterable[int]) -> dict[int, str]:
|
||||
"""Error messages for pins that are missing from the GPIO matrix."""
|
||||
return {num: f"The pin GPIO{num} is not usable on {variant}s." for num in pins}
|
||||
|
||||
|
||||
class ESP32PinSpec:
|
||||
"""Pin facts of one ESP32 variant, with the two validators built from them.
|
||||
|
||||
`error_pins` fail pin validation, which `ignore_pin_validation_error` bypasses;
|
||||
`fatal_pins` and pins above `max_pin` fail both validators, so the option cannot bypass them.
|
||||
`input_only_label` is inserted after "GPIOn" in the input-only errors.
|
||||
A plain class, not a dataclass: building a dataclass costs ~0.5 ms of import.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"error_pins",
|
||||
"fatal_pins",
|
||||
"input_only_label",
|
||||
"input_only_pins",
|
||||
"max_pin",
|
||||
"no_hold_pins",
|
||||
"strapping_pins",
|
||||
"usb_jtag_pins",
|
||||
"warning_pins",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
max_pin: int,
|
||||
strapping_pins: frozenset[int],
|
||||
usb_jtag_pins: frozenset[int] = frozenset(),
|
||||
error_pins: Mapping[int, str] = _NO_PINS,
|
||||
fatal_pins: Mapping[int, str] = _NO_PINS,
|
||||
warning_pins: Mapping[int, str] = _NO_PINS,
|
||||
input_only_pins: frozenset[int] = frozenset(),
|
||||
input_only_label: str = "",
|
||||
no_hold_pins: frozenset[int] = frozenset(),
|
||||
) -> None:
|
||||
self.max_pin = max_pin
|
||||
self.strapping_pins = strapping_pins
|
||||
self.usb_jtag_pins = usb_jtag_pins
|
||||
self.error_pins = error_pins
|
||||
self.fatal_pins = fatal_pins
|
||||
self.warning_pins = warning_pins
|
||||
self.input_only_pins = input_only_pins
|
||||
self.input_only_label = input_only_label
|
||||
self.no_hold_pins = no_hold_pins
|
||||
|
||||
def _check_pin_exists(self, num: int) -> None:
|
||||
if num < 0 or num > self.max_pin:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-{self.max_pin})")
|
||||
if (message := self.fatal_pins.get(num)) is not None:
|
||||
raise cv.Invalid(message)
|
||||
|
||||
def validate_gpio_pin(self, value: int) -> int:
|
||||
self._check_pin_exists(value)
|
||||
if (message := self.error_pins.get(value)) is not None:
|
||||
raise cv.Invalid(message)
|
||||
if (message := self.warning_pins.get(value)) is not None:
|
||||
_LOGGER.warning("%s", message)
|
||||
check_usb_jtag_pin(value, self.usb_jtag_pins, _LOGGER)
|
||||
return value
|
||||
|
||||
def validate_supports(self, value: ConfigType) -> ConfigType:
|
||||
num = value[CONF_NUMBER]
|
||||
self._check_pin_exists(num)
|
||||
hold = value.get(CONF_HOLD_STATE)
|
||||
if num in self.input_only_pins:
|
||||
prefix = f"GPIO{num}{self.input_only_label}"
|
||||
mode = value[CONF_MODE]
|
||||
for key, problem in _INPUT_ONLY_REJECTED_MODES:
|
||||
if mode[key]:
|
||||
raise cv.Invalid(f"{prefix} {problem}", [CONF_MODE, key])
|
||||
if hold:
|
||||
raise cv.Invalid(
|
||||
f"{prefix} is input-only and cannot be held.", [CONF_HOLD_STATE]
|
||||
)
|
||||
if hold and num in self.no_hold_pins:
|
||||
raise cv.Invalid(f"GPIO{num} has no hold function.", [CONF_HOLD_STATE])
|
||||
check_usb_jtag_hold(value, self.usb_jtag_pins, _LOGGER)
|
||||
check_strapping_pin(value, self.strapping_pins, _LOGGER)
|
||||
return value
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Tests for the data driven ESP32 pin validators."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
from esphome.components.esp32.gpio_common import (
|
||||
ESP32PinSpec,
|
||||
spi_psram_errors,
|
||||
unusable_errors,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_IGNORE_STRAPPING_WARNING,
|
||||
CONF_INPUT,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_OPEN_DRAIN,
|
||||
CONF_OUTPUT,
|
||||
CONF_PULLDOWN,
|
||||
CONF_PULLUP,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
SPEC = ESP32PinSpec(
|
||||
max_pin=40,
|
||||
strapping_pins=frozenset({0, 3}),
|
||||
usb_jtag_pins=frozenset({3, 19}),
|
||||
error_pins={
|
||||
**spi_psram_errors("ESP32-X1", {26: "SPICS1"}),
|
||||
**unusable_errors("ESP32-X1", (22,)),
|
||||
},
|
||||
fatal_pins={29: "GPIO29 does not exist on ESP32-X1."},
|
||||
warning_pins={9: "GPIO9 might be used by flash."},
|
||||
input_only_pins=frozenset({34, 35}),
|
||||
input_only_label=" (34-35)",
|
||||
no_hold_pins=frozenset({20}),
|
||||
)
|
||||
|
||||
|
||||
def _conf(num: int, hold: bool = False, **mode: bool) -> ConfigType:
|
||||
return {
|
||||
CONF_NUMBER: num,
|
||||
CONF_MODE: {
|
||||
CONF_INPUT: False,
|
||||
CONF_OUTPUT: False,
|
||||
CONF_PULLUP: False,
|
||||
CONF_PULLDOWN: False,
|
||||
CONF_OPEN_DRAIN: False,
|
||||
**mode,
|
||||
},
|
||||
CONF_HOLD_STATE: hold,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num", [-1, 41])
|
||||
def test_out_of_range_rejected_by_both(num: int) -> None:
|
||||
message = rf"Invalid pin number: {num} \(must be 0-40\)"
|
||||
with pytest.raises(cv.Invalid, match=message):
|
||||
SPEC.validate_gpio_pin(num)
|
||||
with pytest.raises(cv.Invalid, match=message):
|
||||
SPEC.validate_supports(_conf(num, input=True))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("num", "message"),
|
||||
[
|
||||
(
|
||||
26,
|
||||
(
|
||||
"This pin cannot be used on ESP32-X1s and is already used by the"
|
||||
" SPI/PSRAM interface (function: SPICS1)"
|
||||
),
|
||||
),
|
||||
(22, "The pin GPIO22 is not usable on ESP32-X1s."),
|
||||
],
|
||||
)
|
||||
def test_error_pins_only_fail_pin_validation(num: int, message: str) -> None:
|
||||
with pytest.raises(cv.Invalid) as exc:
|
||||
SPEC.validate_gpio_pin(num)
|
||||
assert exc.value.msg == message
|
||||
SPEC.validate_supports(_conf(num, output=True))
|
||||
|
||||
|
||||
def test_fatal_pins_fail_both_validators() -> None:
|
||||
for validate, value in (
|
||||
(SPEC.validate_gpio_pin, 29),
|
||||
(SPEC.validate_supports, _conf(29, input=True)),
|
||||
):
|
||||
with pytest.raises(cv.Invalid, match="GPIO29 does not exist on ESP32-X1."):
|
||||
validate(value)
|
||||
|
||||
|
||||
def test_warning_and_usb_jtag_pins(caplog: pytest.LogCaptureFixture) -> None:
|
||||
with caplog.at_level("WARNING"):
|
||||
assert SPEC.validate_gpio_pin(9) == 9
|
||||
assert SPEC.validate_gpio_pin(19) == 19
|
||||
assert SPEC.validate_gpio_pin(5) == 5
|
||||
assert caplog.messages == [
|
||||
"GPIO9 might be used by flash.",
|
||||
(
|
||||
"GPIO19 is used by the USB-Serial-JTAG interface."
|
||||
" Using this pin as GPIO will conflict with USB-Serial-JTAG."
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "problem"),
|
||||
[
|
||||
(CONF_OUTPUT, "does not support output pin mode."),
|
||||
(CONF_PULLUP, "does not support pullups."),
|
||||
(CONF_PULLDOWN, "does not support pulldowns."),
|
||||
],
|
||||
)
|
||||
def test_input_only_pins_reject_modes(mode: str, problem: str) -> None:
|
||||
with pytest.raises(cv.Invalid) as exc:
|
||||
SPEC.validate_supports(_conf(35, input=True, **{mode: True}))
|
||||
assert exc.value.msg == f"GPIO35 (34-35) {problem}"
|
||||
assert exc.value.path == [CONF_MODE, mode]
|
||||
# The same modes are fine on a normal pin
|
||||
SPEC.validate_supports(_conf(5, input=True, **{mode: True}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("num", "message"),
|
||||
[
|
||||
(34, "GPIO34 (34-35) is input-only and cannot be held."),
|
||||
(20, "GPIO20 has no hold function."),
|
||||
],
|
||||
)
|
||||
def test_pins_that_cannot_hold(num: int, message: str) -> None:
|
||||
SPEC.validate_supports(_conf(num, input=True))
|
||||
with pytest.raises(cv.Invalid) as exc:
|
||||
SPEC.validate_supports(_conf(num, hold=True, input=True))
|
||||
assert exc.value.msg == message
|
||||
assert exc.value.path == [CONF_HOLD_STATE]
|
||||
|
||||
|
||||
def test_usb_jtag_hold_and_strapping_warnings(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
with caplog.at_level("WARNING"):
|
||||
SPEC.validate_supports(_conf(3, hold=True, output=True))
|
||||
assert len(caplog.messages) == 2
|
||||
assert caplog.messages[0] == (
|
||||
"GPIO3 cannot hold at low level during wakeup from deep sleep."
|
||||
)
|
||||
assert caplog.messages[1].startswith("GPIO3 is a strapping pin")
|
||||
|
||||
|
||||
def test_ignore_strapping_warning_on_normal_pin_rejected() -> None:
|
||||
conf = _conf(5, input=True)
|
||||
conf[CONF_IGNORE_STRAPPING_WARNING] = True
|
||||
with pytest.raises(cv.Invalid, match="GPIO5 is not a strapping pin"):
|
||||
SPEC.validate_supports(conf)
|
||||
Reference in new issue
Block a user