mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 18:18:43 +00:00
[remote_base] Accept protocols registered by external components (#19332)
This commit is contained in:
@@ -169,6 +169,12 @@ def request_protocol(name: str) -> None:
|
||||
cg.add_define(protocol_define(name))
|
||||
|
||||
|
||||
def _request_protocol_if_in_tree(name: str) -> None:
|
||||
"""Registry names from external components have no source file here and need no define."""
|
||||
if _protocol_stem(name) in _PROTOCOL_STEMS:
|
||||
request_protocol(name)
|
||||
|
||||
|
||||
# Only the protocol sources a configuration uses are compiled
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{f"{stem}_protocol.cpp": protocol_define(stem) for stem in _PROTOCOL_STEMS}
|
||||
@@ -182,7 +188,7 @@ def register_binary_sensor(
|
||||
|
||||
def decorator(func: Callable[[MockObj, ConfigType], Any]) -> Callable:
|
||||
async def new_func(var: MockObj, config: ConfigType) -> None:
|
||||
request_protocol(name)
|
||||
_request_protocol_if_in_tree(name)
|
||||
await coroutine(func)(var, config)
|
||||
|
||||
return registerer(new_func)
|
||||
@@ -200,7 +206,7 @@ def register_trigger(name, type, data_type):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config):
|
||||
request_protocol(name)
|
||||
_request_protocol_if_in_tree(name)
|
||||
var = cg.new_Pvariable(config[CONF_TRIGGER_ID])
|
||||
await coroutine(func)(var, config)
|
||||
await automation.build_automation(var, [(data_type, "x")], config)
|
||||
@@ -218,7 +224,7 @@ def register_dumper(name, type, schema=None):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config, dumper_id):
|
||||
request_protocol(name)
|
||||
_request_protocol_if_in_tree(name)
|
||||
var = cg.new_Pvariable(dumper_id)
|
||||
await coroutine(func)(var, config)
|
||||
return var
|
||||
@@ -259,7 +265,7 @@ def register_action(name, type_, schema):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config, action_id, template_arg, args):
|
||||
request_protocol(name)
|
||||
_request_protocol_if_in_tree(name)
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await register_transmittable(var, config)
|
||||
if CONF_REPEAT in config:
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
logger:
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: ../external_components
|
||||
|
||||
fake_protocol:
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: GPIO4
|
||||
dump:
|
||||
- fake
|
||||
- nec
|
||||
on_fake:
|
||||
then:
|
||||
- remote_transmitter.transmit_fake:
|
||||
on_nec:
|
||||
then:
|
||||
- logger.log: nec
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
binary_sensor:
|
||||
- platform: remote_receiver
|
||||
name: Fake Input
|
||||
fake:
|
||||
@@ -0,0 +1,39 @@
|
||||
"""External component registering a protocol that has no source file in remote_base."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import remote_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["remote_base"]
|
||||
|
||||
ns = cg.esphome_ns.namespace("fake_protocol")
|
||||
FakeData = ns.struct("FakeData")
|
||||
FakeBinarySensor = ns.class_(
|
||||
"FakeBinarySensor", remote_base.RemoteReceiverBinarySensorBase
|
||||
)
|
||||
FakeTrigger = ns.class_("FakeTrigger", remote_base.RemoteReceiverTrigger)
|
||||
FakeAction = ns.class_("FakeAction", remote_base.RemoteTransmitterActionBase)
|
||||
FakeDumper = ns.class_("FakeDumper", remote_base.RemoteReceiverDumperBase)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({})
|
||||
|
||||
|
||||
@remote_base.register_binary_sensor("fake", FakeBinarySensor, {})
|
||||
def fake_binary_sensor(var: cg.MockObj, config: ConfigType) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@remote_base.register_trigger("fake", FakeTrigger, FakeData)
|
||||
def fake_trigger(var: cg.MockObj, config: ConfigType) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@remote_base.register_dumper("fake", FakeDumper)
|
||||
def fake_dumper(var: cg.MockObj, config: ConfigType) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@remote_base.register_action("fake", FakeAction, {})
|
||||
async def fake_action(var: cg.MockObj, config: ConfigType, args: list) -> None:
|
||||
pass
|
||||
@@ -1,13 +1,16 @@
|
||||
"""Listener and dumper StaticVector sizes come from codegen slot counts."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Generator
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import loader
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
from esphome.components import remote_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
|
||||
from ..helpers import get_define_value
|
||||
|
||||
@@ -74,6 +77,47 @@ def test_every_registry_name_maps_to_a_protocol_source() -> None:
|
||||
assert remote_base._protocol_stem(name) in remote_base._PROTOCOL_STEMS, name
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def restore_protocol_registries() -> Generator[None]:
|
||||
"""Loading an external protocol component adds to module-level registries; undo that.
|
||||
|
||||
The loader caches the component too, so drop it or a second load would skip the
|
||||
decorators and leave the restored registries without the external names.
|
||||
"""
|
||||
registries = (
|
||||
remote_base.BINARY_SENSOR_REGISTRY,
|
||||
remote_base.TRIGGER_REGISTRY,
|
||||
remote_base.DUMPER_REGISTRY,
|
||||
ACTION_REGISTRY,
|
||||
)
|
||||
saved = [dict(registry) for registry in registries]
|
||||
yield
|
||||
for registry, entries in zip(registries, saved, strict=True):
|
||||
registry.clear()
|
||||
registry.update(entries)
|
||||
loader._COMPONENT_CACHE.pop("fake_protocol", None)
|
||||
sys.modules.pop("esphome.components.fake_protocol", None)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("restore_protocol_registries")
|
||||
def test_external_protocols_register_without_a_remote_base_source(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""An external protocol goes through all four decorators without a source file here, so no define is emitted."""
|
||||
main_cpp = generate_main(
|
||||
component_config_path("receiver_with_external_protocol.yaml")
|
||||
)
|
||||
defines = {define.name for define in CORE.defines}
|
||||
assert "USE_REMOTE_PROTOCOL_NEC" in defines
|
||||
assert "USE_REMOTE_PROTOCOL_FAKE" not in defines
|
||||
for cls in ("FakeBinarySensor", "FakeTrigger", "FakeDumper", "FakeAction"):
|
||||
assert f"fake_protocol::{cls}" in main_cpp, cls
|
||||
# fake and nec dumpers; on_fake and on_nec triggers plus the fake binary sensor
|
||||
assert get_define_value("REMOTE_BASE_DUMPER_COUNT") == "2"
|
||||
assert get_define_value("REMOTE_BASE_LISTENER_COUNT") == "3"
|
||||
|
||||
|
||||
def test_request_protocol_rejects_unknown_names() -> None:
|
||||
"""A misspelled protocol would otherwise surface only as a link error."""
|
||||
with pytest.raises(ValueError, match="Unknown remote protocol 'toshiba'"):
|
||||
|
||||
Reference in New Issue
Block a user