Files
esphome/tests/component_tests/tca8418/test_final_validate.py
T
a68cf669aa [tca8418] Add TCA8418 keypad component (#18521)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-12 12:39:11 -10:00

157 lines
4.8 KiB
Python

"""Tests for how the tca8418 binary sensor resolves its key against the keypad."""
from __future__ import annotations
import pytest
from esphome.components.const import CONF_COLUMNS, CONF_KEYS, CONF_ROWS
from esphome.components.tca8418 import CONF_GPI_EVENTS
from esphome.components.tca8418.binary_sensor import (
CONF_KEY_CODE,
CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA,
)
from esphome.config import Config
import esphome.config_validation as cv
from esphome.const import (
CONF_COL,
CONF_ID,
CONF_KEY,
CONF_NAME,
CONF_ROW,
PlatformFramework,
)
from esphome.core import ID
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
HUB_ID = "keypad"
def _full_config(
*,
rows: int = 4,
columns: int = 3,
keys: str | None = "123456789*0#",
gpi_events: bool = True,
) -> Config:
"""A full config carrying one keypad, as the ID pass leaves it."""
hub: ConfigType = {
CONF_ID: ID(HUB_ID, is_declaration=True, type="tca8418"),
CONF_ROWS: rows,
CONF_COLUMNS: columns,
CONF_GPI_EVENTS: gpi_events,
}
if keys is not None:
hub[CONF_KEYS] = keys
full = Config()
full["tca8418"] = [hub]
full.declare_ids.append((hub[CONF_ID], ["tca8418", 0, CONF_ID]))
return full
def _sensor(**selector: object) -> ConfigType:
config: ConfigType = {
"keypad_id": ID(HUB_ID, is_declaration=False, type="tca8418"),
CONF_NAME: "Key",
}
config.update(selector)
return config
def _validated(config: ConfigType) -> ConfigType:
config = CONFIG_SCHEMA(config)
FINAL_VALIDATE_SCHEMA(config)
return config
@pytest.fixture
def keypad(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(PlatformFramework.ESP32_IDF, full_config=_full_config())
# A key number is worked out here so the sensor only compares numbers at run
# time. Matrix keys are row * 10 + column + 1.
@pytest.mark.parametrize(
("selector", "key_code"),
[
({CONF_ROW: 0, CONF_COL: 0}, 1),
({CONF_ROW: 3, CONF_COL: 2}, 33),
({CONF_KEY: "1"}, 1),
({CONF_KEY: "5"}, 12),
({CONF_KEY: "#"}, 33),
({CONF_KEY_CODE: 12}, 12),
],
)
def test_key_is_resolved(keypad: None, selector: ConfigType, key_code: int) -> None:
assert _validated(_sensor(**selector))[CONF_KEY_CODE] == key_code
def test_position_outside_the_matrix_is_rejected(keypad: None) -> None:
with pytest.raises(cv.Invalid, match="outside the 4 x 3 key matrix"):
_validated(_sensor(row=0, col=3))
def test_key_number_outside_the_matrix_is_rejected(keypad: None) -> None:
"""Key 5 is ROW0/COL4, which a three column matrix never reports."""
with pytest.raises(cv.Invalid, match="position 0/4, which is outside"):
_validated(_sensor(key_code=5))
def test_key_number_of_a_pin_the_matrix_uses_is_rejected(keypad: None) -> None:
"""97 is ROW0, which belongs to the matrix, so it cannot report on its own."""
with pytest.raises(cv.Invalid, match="ROW0, which the key matrix is using"):
_validated(_sensor(key_code=97))
def test_individual_input_outside_the_matrix_passes(keypad: None) -> None:
"""ROW5 is past the four rows of the matrix, so it reports on its own."""
assert _validated(_sensor(key_code=102))[CONF_KEY_CODE] == 102
def test_column_beyond_the_matrix_passes(keypad: None) -> None:
"""COL3 is past the three columns of the matrix; 105 is COL0."""
assert _validated(_sensor(key_code=108))[CONF_KEY_CODE] == 108
def test_individual_input_needs_gpi_events(
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF, full_config=_full_config(gpi_events=False)
)
with pytest.raises(cv.Invalid, match="'gpi_events' turned off"):
_validated(_sensor(key_code=102))
def test_character_without_a_key_map_is_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(PlatformFramework.ESP32_IDF, full_config=_full_config(keys=None))
with pytest.raises(cv.Invalid, match="which is not set"):
_validated(_sensor(key="5"))
def test_character_not_in_the_key_map_is_rejected(keypad: None) -> None:
with pytest.raises(cv.Invalid, match="not one of the keypad's keys"):
_validated(_sensor(key="Z"))
def test_repeated_character_is_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A character appearing twice does not name one key."""
set_core_config(
PlatformFramework.ESP32_IDF, full_config=_full_config(keys="1234567891*0")
)
with pytest.raises(cv.Invalid, match="appears more than once"):
_validated(_sensor(key="1"))
def test_key_given_as_a_number_is_accepted(keypad: None) -> None:
"""'key: 5' is a reasonable way to name the key labelled 5."""
assert _validated(_sensor(key=5))[CONF_KEY_CODE] == 12