mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
[remote_base] Compile only the protocol sources a configuration uses
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir
|
||||
from esphome.components import climate_ir, remote_base
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
@@ -12,4 +12,5 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
remote_base.request_protocol("coolix") # used from C++
|
||||
await climate_ir.new_climate_ir(config)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate, remote_transmitter, sensor, uart
|
||||
from esphome.components import climate, remote_base, remote_transmitter, sensor, uart
|
||||
from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode
|
||||
from esphome.components.remote_base import CONF_TRANSMITTER_ID
|
||||
import esphome.config_validation as cv
|
||||
@@ -280,6 +280,7 @@ async def to_code(config):
|
||||
cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds))
|
||||
cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS]))
|
||||
if CONF_TRANSMITTER_ID in config:
|
||||
remote_base.request_protocol("midea") # ir_transmitter.h uses it from C++
|
||||
cg.add_define("USE_REMOTE_TRANSMITTER")
|
||||
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
|
||||
cg.add(var.set_transmitter(transmitter_))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir
|
||||
from esphome.components import climate_ir, remote_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_USE_FAHRENHEIT
|
||||
from esphome.types import ConfigType
|
||||
@@ -19,5 +19,9 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# midea_ir uses MideaProtocol from C++ and auto-loads coolix, whose coolix.cpp uses
|
||||
# CoolixProtocol even when no coolix climate is configured
|
||||
remote_base.request_protocol("midea")
|
||||
remote_base.request_protocol("coolix")
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT]))
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
@@ -120,8 +122,45 @@ async def register_transmittable(var, config):
|
||||
cg.add(var.set_transmitter(transmitter_))
|
||||
|
||||
|
||||
# Registry names that share a protocol source file
|
||||
def _protocol_stem(name: str) -> str:
|
||||
if name.startswith("rc_switch"):
|
||||
return "rc_switch"
|
||||
if name == "canalsatld":
|
||||
return "canalsat"
|
||||
return name
|
||||
|
||||
|
||||
def protocol_define(name: str) -> str:
|
||||
return f"USE_REMOTE_PROTOCOL_{_protocol_stem(name).upper()}"
|
||||
|
||||
|
||||
def request_protocol(name: str) -> None:
|
||||
"""Keep a protocol's source file in the build; components using it from C++ must call this."""
|
||||
cg.add_define(protocol_define(name))
|
||||
|
||||
|
||||
_PROTOCOL_STEMS = sorted(
|
||||
path.name.removesuffix("_protocol.cpp")
|
||||
for path in Path(__file__).parent.glob("*_protocol.cpp")
|
||||
)
|
||||
# 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}
|
||||
)
|
||||
|
||||
|
||||
def register_binary_sensor(name, type, schema):
|
||||
return BINARY_SENSOR_REGISTRY.register(name, type, schema)
|
||||
registerer = BINARY_SENSOR_REGISTRY.register(name, type, schema)
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(var, config):
|
||||
request_protocol(name)
|
||||
await coroutine(func)(var, config)
|
||||
|
||||
return registerer(new_func)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_trigger(name, type, data_type):
|
||||
@@ -134,6 +173,7 @@ def register_trigger(name, type, data_type):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config):
|
||||
request_protocol(name)
|
||||
var = cg.new_Pvariable(config[CONF_TRIGGER_ID])
|
||||
await coroutine(func)(var, config)
|
||||
await automation.build_automation(var, [(data_type, "x")], config)
|
||||
@@ -151,6 +191,7 @@ def register_dumper(name, type, schema=None):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config, dumper_id):
|
||||
request_protocol(name)
|
||||
var = cg.new_Pvariable(dumper_id)
|
||||
await coroutine(func)(var, config)
|
||||
return var
|
||||
@@ -191,6 +232,7 @@ def register_action(name, type_, schema):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config, action_id, template_arg, args):
|
||||
request_protocol(name)
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await register_transmittable(var, config)
|
||||
if CONF_REPEAT in config:
|
||||
|
||||
@@ -102,7 +102,7 @@ bool RemoteReceiverBinarySensorBase::on_receive(RemoteReceiveData src) {
|
||||
// Slots are counted at code generation; a registration from C++ setup() has none
|
||||
#ifdef REMOTE_BASE_LISTENER_COUNT
|
||||
void RemoteReceiverBase::register_listener(RemoteReceiverListener *listener) {
|
||||
if (this->listeners_.size() == REMOTE_BASE_LISTENER_COUNT) {
|
||||
if (this->listeners_.size() == this->listeners_.capacity()) {
|
||||
ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("listener"),
|
||||
LOG_STR_LITERAL("listener"));
|
||||
return;
|
||||
@@ -117,7 +117,7 @@ void RemoteReceiverBase::register_dumper(RemoteReceiverDumperBase *dumper) {
|
||||
this->secondary_dumper_ = dumper;
|
||||
return;
|
||||
}
|
||||
if (this->dumpers_.size() == REMOTE_BASE_DUMPER_COUNT) {
|
||||
if (this->dumpers_.size() == this->dumpers_.capacity()) {
|
||||
ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("dumper"),
|
||||
LOG_STR_LITERAL("dumper"));
|
||||
return;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <concepts>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir
|
||||
from esphome.components import climate_ir, remote_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MODEL
|
||||
from esphome.types import ConfigType
|
||||
@@ -26,5 +26,6 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).exten
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
remote_base.request_protocol("toshiba_ac") # used from C++
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
|
||||
@@ -139,6 +139,41 @@
|
||||
#define MK2PVROUTER_LISTENER_COUNT 1
|
||||
#define REMOTE_BASE_DUMPER_COUNT 1
|
||||
#define REMOTE_BASE_LISTENER_COUNT 1
|
||||
#define USE_REMOTE_PROTOCOL_ABBWELCOME
|
||||
#define USE_REMOTE_PROTOCOL_AEHA
|
||||
#define USE_REMOTE_PROTOCOL_BEO4
|
||||
#define USE_REMOTE_PROTOCOL_BRENNENSTUHL
|
||||
#define USE_REMOTE_PROTOCOL_BYRONSX
|
||||
#define USE_REMOTE_PROTOCOL_CANALSAT
|
||||
#define USE_REMOTE_PROTOCOL_COOLIX
|
||||
#define USE_REMOTE_PROTOCOL_DISH
|
||||
#define USE_REMOTE_PROTOCOL_DOOYA
|
||||
#define USE_REMOTE_PROTOCOL_DRAYTON
|
||||
#define USE_REMOTE_PROTOCOL_DYSON
|
||||
#define USE_REMOTE_PROTOCOL_GOBOX
|
||||
#define USE_REMOTE_PROTOCOL_HAIER
|
||||
#define USE_REMOTE_PROTOCOL_JVC
|
||||
#define USE_REMOTE_PROTOCOL_KEELOQ
|
||||
#define USE_REMOTE_PROTOCOL_LG
|
||||
#define USE_REMOTE_PROTOCOL_MAGIQUEST
|
||||
#define USE_REMOTE_PROTOCOL_MIDEA
|
||||
#define USE_REMOTE_PROTOCOL_MIRAGE
|
||||
#define USE_REMOTE_PROTOCOL_NEC
|
||||
#define USE_REMOTE_PROTOCOL_NEXA
|
||||
#define USE_REMOTE_PROTOCOL_PANASONIC
|
||||
#define USE_REMOTE_PROTOCOL_PIONEER
|
||||
#define USE_REMOTE_PROTOCOL_PRONTO
|
||||
#define USE_REMOTE_PROTOCOL_RAW
|
||||
#define USE_REMOTE_PROTOCOL_RC5
|
||||
#define USE_REMOTE_PROTOCOL_RC6
|
||||
#define USE_REMOTE_PROTOCOL_RC_SWITCH
|
||||
#define USE_REMOTE_PROTOCOL_ROOMBA
|
||||
#define USE_REMOTE_PROTOCOL_SAMSUNG
|
||||
#define USE_REMOTE_PROTOCOL_SAMSUNG36
|
||||
#define USE_REMOTE_PROTOCOL_SONY
|
||||
#define USE_REMOTE_PROTOCOL_SYMPHONY
|
||||
#define USE_REMOTE_PROTOCOL_TOSHIBA_AC
|
||||
#define USE_REMOTE_PROTOCOL_TOTO
|
||||
#define SERIAL_PROXY_COUNT 2
|
||||
#define SNTP_SERVER_COUNT 3
|
||||
#define USE_MEDIA_PLAYER
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import remote_base
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from ..helpers import get_define_value
|
||||
|
||||
|
||||
@@ -34,3 +39,44 @@ def test_proxy_receivers_count_as_listeners(
|
||||
# infrared and radio_frequency ir_rf_proxy platforms each listen
|
||||
assert get_define_value("REMOTE_BASE_LISTENER_COUNT") == "2"
|
||||
assert get_define_value("REMOTE_BASE_DUMPER_COUNT") is None
|
||||
|
||||
|
||||
def test_only_used_protocol_sources_are_compiled(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("receiver_with_dumpers.yaml"))
|
||||
excluded = set(remote_base.FILTER_SOURCE_FILES())
|
||||
assert "nec_protocol.cpp" not in excluded
|
||||
assert "rc_switch_protocol.cpp" not in excluded
|
||||
assert "sony_protocol.cpp" in excluded
|
||||
assert "remote_base.cpp" not in excluded
|
||||
|
||||
|
||||
def test_every_registry_name_maps_to_a_protocol_source() -> None:
|
||||
sources = {
|
||||
path.name for path in Path(remote_base.__file__).parent.glob("*_protocol.cpp")
|
||||
}
|
||||
names = (
|
||||
set(remote_base.BINARY_SENSOR_REGISTRY)
|
||||
| set(remote_base.DUMPER_REGISTRY)
|
||||
| {key.removeprefix("on_") for key in remote_base.TRIGGER_REGISTRY}
|
||||
)
|
||||
for name in names:
|
||||
stem = (
|
||||
remote_base.protocol_define(name)
|
||||
.removeprefix("USE_REMOTE_PROTOCOL_")
|
||||
.lower()
|
||||
)
|
||||
assert f"{stem}_protocol.cpp" in sources, name
|
||||
|
||||
|
||||
def test_dump_list_is_deduplicated_across_forms() -> None:
|
||||
dumpers = remote_base.validate_dumpers(["raw", {"raw": None}, "nec", "nec"])
|
||||
assert [name for name, _ in dumpers] == ["raw", "nec"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", [["nec", None], [5]])
|
||||
def test_dump_list_rejects_invalid_entries_with_a_validation_error(bad: list) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
remote_base.validate_dumpers(bad)
|
||||
|
||||
Reference in New Issue
Block a user