Merge branch 'dev' into time-filter-posix-tz

This commit is contained in:
J. Nick Koston
2026-08-23 21:00:37 -05:00
committed by GitHub
656 changed files with 6113 additions and 2962 deletions
+1
View File
@@ -381,6 +381,7 @@ esphome/components/nextion/switch/* @senexcrenshaw
esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra
esphome/components/noise/* @esphome/core
esphome/components/npi19/* @bakerkj
esphome/components/nrf52/* @tomaszduda23
esphome/components/number/* @esphome/core
+2 -1
View File
@@ -6,6 +6,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_METER,
)
from esphome.types import ConfigType
CODEOWNERS = ["@MrSuicideParrot"]
DEPENDENCIES = ["uart"]
@@ -35,7 +36,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
+2 -1
View File
@@ -6,6 +6,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_MILLIMETER,
)
from esphome.types import ConfigType
CODEOWNERS = ["@TH-Braemer"]
DEPENDENCIES = ["uart"]
@@ -35,7 +36,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
+2 -1
View File
@@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import stepper
import esphome.config_validation as cv
from esphome.const import CONF_DIR_PIN, CONF_ID, CONF_SLEEP_PIN, CONF_STEP_PIN
from esphome.types import ConfigType
a4988_ns = cg.esphome_ns.namespace("a4988")
A4988 = a4988_ns.class_("A4988", stepper.Stepper, cg.Component)
@@ -17,7 +18,7 @@ CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend(
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await stepper.register_stepper(var, config)
@@ -9,6 +9,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_GRAMS_PER_CUBIC_METER,
)
from esphome.types import ConfigType
absolute_humidity_ns = cg.esphome_ns.namespace("absolute_humidity")
AbsoluteHumidityComponent = absolute_humidity_ns.class_(
@@ -43,7 +44,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
+2 -1
View File
@@ -4,6 +4,7 @@ from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER
from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@glmnet"]
@@ -48,7 +49,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
if CORE.is_esp32:
from esphome.components.esp32 import include_builtin_idf_component
+4 -1
View File
@@ -4,6 +4,9 @@ from esphome.components.light.effects import register_addressable_effect
from esphome.components.light.types import AddressableLightEffect
import esphome.config_validation as cv
from esphome.const import CONF_NAME, CONF_UART_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
DEPENDENCIES = ["uart"]
@@ -21,7 +24,7 @@ CONFIG_SCHEMA = cv.Schema({})
"Adalight",
{cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent)},
)
async def adalight_light_effect_to_code(config, effect_id):
async def adalight_light_effect_to_code(config: ConfigType, effect_id: ID) -> MockObj:
effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
await uart.register_uart_device(effect, config)
return effect
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import spi
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["spi"]
MULTI_CONF = True
@@ -17,7 +18,7 @@ CONFIG_SCHEMA = cv.Schema(
).extend(spi.spi_device_schema(cs_pin_required=True))
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.types import ConfigType
from .. import ADC128S102, adc128s102_ns
@@ -28,7 +29,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_CHANNEL],
@@ -11,6 +11,7 @@ from esphome.const import (
CONF_UPDATE_INTERVAL,
CONF_WIDTH,
)
from esphome.types import ConfigType
CODEOWNERS = ["@justfalter"]
@@ -38,7 +39,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
wrapped_light = await cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID])
cg.add(var.set_width(config[CONF_WIDTH]))
+4 -3
View File
@@ -36,6 +36,7 @@ from esphome.const import (
UNIT_WATT,
UNIT_WATT_HOURS,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
@@ -243,7 +244,7 @@ CONFIG_SCHEMA = cv.All(
)
async def neutral_channel(config):
async def neutral_channel(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
current = config[CONF_CURRENT]
@@ -257,7 +258,7 @@ async def neutral_channel(config):
return var
async def power_channel(config):
async def power_channel(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
for sensor_type in POWER_SENSOR_TYPES:
@@ -280,7 +281,7 @@ async def power_channel(config):
return var
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+3 -1
View File
@@ -23,6 +23,8 @@ from esphome.const import (
UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@angelnu"]
@@ -163,7 +165,7 @@ ADE7953_CONFIG_SCHEMA = cv.Schema(
).extend(cv.polling_component_schema("60s"))
async def register_ade7953(var, config):
async def register_ade7953(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
if irq_pin_config := config.get(CONF_IRQ_PIN):
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import ade7953_base, i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["ade7953_base"]
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await i2c.register_i2c_device(var, config)
await ade7953_base.register_ade7953(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import ade7953_base, spi
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["spi"]
AUTO_LOAD = ["ade7953_base"]
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config)
await ade7953_base.register_ade7953(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
@@ -24,7 +25,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
@@ -11,6 +11,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_VOLT,
)
from esphome.types import ConfigType
from .. import CONF_ADS1115_ID, ADS1115Component, ads1115_ns
@@ -86,7 +87,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await sensor.register_sensor(var, config)
await cg.register_component(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import spi
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
CODEOWNERS = ["@solomondg1"]
DEPENDENCIES = ["spi"]
@@ -23,7 +24,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
@@ -11,6 +11,7 @@ from esphome.const import (
UNIT_CELSIUS,
UNIT_VOLT,
)
from esphome.types import ConfigType
from .. import ADS1118, CONF_ADS1118_ID, ads1118_ns
@@ -86,7 +87,7 @@ CONFIG_SCHEMA = cv.typed_schema(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_ADS1118_ID])
+2 -1
View File
@@ -12,6 +12,7 @@ from esphome.const import (
UNIT_CELSIUS,
UNIT_PERCENT,
)
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
@@ -50,7 +51,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import ble_device_base
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
AUTO_LOAD = ["ble_device_base"]
CODEOWNERS = ["@jeromelaban"]
@@ -21,6 +22,6 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await ble_device_base.register_ble_device(var, config)
@@ -20,6 +20,8 @@ from esphome.const import (
UNIT_PERCENT,
UNIT_VOLT,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@ncareau", "@jeromelaban"]
@@ -78,7 +80,7 @@ BASE_SCHEMA = (
)
async def wave_base_to_code(var, config):
async def wave_base_to_code(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import airthings_wave_base
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
@@ -20,6 +21,6 @@ CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await airthings_wave_base.wave_base_to_code(var, config)
@@ -83,7 +83,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await airthings_wave_base.wave_base_to_code(var, config)
+2 -1
View File
@@ -20,6 +20,7 @@ from esphome.const import (
UNIT_VOLT,
UNIT_WATT,
)
from esphome.types import ConfigType
alpha3_ns = cg.esphome_ns.namespace("alpha3")
Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent)
@@ -68,7 +69,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
+2 -1
View File
@@ -11,6 +11,7 @@ from esphome.const import (
UNIT_CELSIUS,
UNIT_PERCENT,
)
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
@@ -40,7 +41,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -11,6 +11,7 @@ from esphome.const import (
UNIT_CELSIUS,
UNIT_PERCENT,
)
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
@@ -42,7 +43,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import ble_client, cover
import esphome.config_validation as cv
from esphome.const import CONF_PIN
from esphome.types import ConfigType
CODEOWNERS = ["@buxtronix"]
DEPENDENCIES = ["ble_client"]
@@ -27,7 +28,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await cover.new_cover(config)
cg.add(var.set_pin(config[CONF_PIN]))
cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
+2 -1
View File
@@ -11,6 +11,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
)
from esphome.types import ConfigType
AUTO_LOAD = ["am43"]
CODEOWNERS = ["@buxtronix"]
@@ -42,7 +43,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import binary_sensor, sensor
import esphome.config_validation as cv
from esphome.const import CONF_SENSOR_ID, CONF_THRESHOLD
from esphome.types import ConfigType
analog_threshold_ns = cg.esphome_ns.namespace("analog_threshold")
@@ -32,7 +33,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import ble_client, climate
import esphome.config_validation as cv
from esphome.const import CONF_UNIT_OF_MEASUREMENT
from esphome.types import ConfigType
UNITS = {
"f": "f",
@@ -28,7 +29,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await climate.new_climate(config)
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
+6 -2
View File
@@ -1,6 +1,8 @@
# Based on this datasheet:
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
from typing import Any
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
@@ -11,6 +13,8 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_LUX,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
@@ -55,7 +59,7 @@ AMBIENT_LIGHT_GAINS = {
}
def _validate_measurement_rate(value):
def _validate_measurement_rate(value: Any) -> MockObj:
value = cv.positive_time_period_milliseconds(value)
return cv.enum(MEASUREMENT_RATES, int=True)(value.total_milliseconds)
@@ -85,7 +89,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+36 -56
View File
@@ -1,4 +1,3 @@
import base64
import logging
from typing import Any
@@ -6,7 +5,16 @@ from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.logger import request_log_listener
from esphome.config_helpers import get_logger_level
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
# components and downstream consumers that import them from api
from esphome.components.noise import ( # noqa: F401
ENCRYPTION_SCHEMA,
decode_encryption_key,
encryption_schema,
validate_encryption_key,
)
from esphome.config_helpers import filter_source_files_from_defines, get_logger_level
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTION,
@@ -38,6 +46,10 @@ from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_pr
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigFragmentType, ConfigType
# Compat alias: downstream consumers (e.g. device-builder) referenced the
# schema by its old private name before it moved to the noise component
_encryption_schema = encryption_schema
_LOGGER = logging.getLogger(__name__)
DOMAIN = "api"
@@ -46,9 +58,15 @@ CODEOWNERS = ["@esphome/core"]
def AUTO_LOAD(config: ConfigType) -> list[str]:
"""Conditionally auto-load json only when capture_response is used."""
"""Conditionally auto-load noise (encryption) and json (capture_response)."""
base = ["socket"]
# A falsy config is a tooling probe for the maximal set (None from
# dependency resolution, {} from the components-graph platform probe);
# a validated config always carries defaults, never empty
if not config or CONF_ENCRYPTION in config:
base = base + ["noise"]
# Check if any homeassistant.action/homeassistant.service has capture_response: true
# This flag is set during config validation in _validate_response_config
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
@@ -130,20 +148,6 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
return config
def validate_encryption_key(value: Any) -> str:
value = cv.string_strict(value)
try:
decoded = base64.b64decode(value, validate=True)
except ValueError as err:
raise cv.Invalid("Invalid key format, please check it's using base64") from err
if len(decoded) != 32:
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
# Return original data for roundtrip conversion
return value
CONF_SUPPORTS_RESPONSE = "supports_response"
# Enum values in api::enums namespace
@@ -250,18 +254,6 @@ ACTIONS_SCHEMA = automation.validate_automation(
),
)
ENCRYPTION_SCHEMA = cv.Schema(
{
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
}
)
def _encryption_schema(config: ConfigType | None) -> ConfigType:
if config is None:
config = {}
return ENCRYPTION_SCHEMA(config)
def _consume_api_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for API component."""
@@ -297,7 +289,7 @@ CONFIG_SCHEMA = cv.All(
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
cv.Optional(CONF_ENCRYPTION): _encryption_schema,
cv.Optional(CONF_ENCRYPTION): encryption_schema,
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
@@ -484,7 +476,7 @@ async def to_code(config: ConfigType) -> None:
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
if key := encryption_config.get(CONF_KEY):
decoded = base64.b64decode(key)
decoded = decode_encryption_key(key)
cg.add(var.set_noise_psk(list(decoded)))
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
else:
@@ -498,10 +490,6 @@ async def to_code(config: ConfigType) -> None:
# and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.21")
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
else:
cg.add_define("USE_API_PLAINTEXT")
@@ -847,11 +835,20 @@ async def api_connected_to_code(
return var
# user_services.cpp is only needed when user defined actions exist; the
# frame helpers are fully #ifdef'd on the protocol defines set in to_code
# (both are set when encryption is configured without a key).
_define_filter = filter_source_files_from_defines(
{
"user_services.cpp": "USE_API_USER_DEFINED_ACTIONS",
"api_frame_helper_noise.cpp": "USE_API_NOISE",
"api_frame_helper_plaintext.cpp": "USE_API_PLAINTEXT",
}
)
def FILTER_SOURCE_FILES() -> list[str]:
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled,
user_services.cpp when no services are defined, and protocol-specific
implementations based on encryption configuration."""
files_to_filter: list[str] = []
files_to_filter = _define_filter()
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
# This is a particularly large file that still needs to be opened and read
@@ -862,21 +859,4 @@ def FILTER_SOURCE_FILES() -> list[str]:
if get_logger_level() != "VERY_VERBOSE":
files_to_filter.append("api_pb2_dump.cpp")
# user_services.cpp is only needed when services are defined
config = CORE.config.get(DOMAIN, {})
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
files_to_filter.append("user_services.cpp")
# Filter protocol-specific implementations based on encryption configuration
encryption_config = config.get(CONF_ENCRYPTION) if config else None
# If encryption is not configured at all, we only need plaintext
if encryption_config is None:
files_to_filter.append("api_frame_helper_noise.cpp")
# If encryption is configured with a key, we only need noise
elif encryption_config.get(CONF_KEY):
files_to_filter.append("api_frame_helper_plaintext.cpp")
# If encryption is configured but no key is provided, we need both
# (this allows a plaintext client to provide a noise key)
return files_to_filter
+6 -2
View File
@@ -1002,8 +1002,12 @@ message GetTimeResponse {
option (no_delay) = true;
fixed32 epoch_seconds = 1;
string timezone = 2;
ParsedTimezone parsed_timezone = 3;
// Deprecated in 2026.9.0: clients still send this string for older firmware,
// but new firmware only reads parsed_timezone. Clients older than Home
// Assistant 2026.3.0 that send only the string leave the device on its
// codegen-configured timezone (or UTC).
string timezone = 2 [deprecated = true];
ParsedTimezone parsed_timezone = 3 [(track_presence) = true];
}
// ==================== USER-DEFINES SERVICES ====================
+31 -34
View File
@@ -412,15 +412,15 @@ void APIConnection::finalize_iterator_sync_() {
}
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
size_t initial_size = this->deferred_batch_.size();
size_t max_batch = MAX_INITIAL_PER_BATCH;
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
iterator.advance();
}
// Budget by remaining batch capacity so a pass cannot overfill the batch;
// stops early on a refused send and resumes next loop pass
size_t batch_size = this->deferred_batch_.size();
if (batch_size < MAX_INITIAL_BATCH_SIZE)
iterator.try_advance(MAX_INITIAL_BATCH_SIZE - batch_size);
// If the batch is full, process it immediately
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
if (this->deferred_batch_.size() >= max_batch) {
// Flush immediately once enough is queued (not guaranteed every pass);
// partial batches go out via the batch timer or finalize_iterator_sync_()
if (this->deferred_batch_.size() >= MAX_INITIAL_BATCH_SIZE) {
this->process_batch_();
}
}
@@ -1204,31 +1204,28 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
if (homeassistant::global_homeassistant_time != nullptr) {
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
#if defined(USE_HOMEASSISTANT_TIMEZONE) && defined(USE_TIME_TIMEZONE)
if (!value.timezone.empty()) {
// Check if the sender provided pre-parsed timezone data.
// If std_offset is non-zero or DST rules are present, the parsed data was populated.
// For UTC (all zeros), string parsing produces the same result, so the fallback is equivalent.
// Apply only if the sender provided pre-parsed timezone data (Home Assistant 2026.3.0
// and newer); field presence distinguishes a genuine all-zero UTC timezone from an
// absent field. Older clients send only the deprecated timezone string, which is no
// longer decoded; for them the device keeps its codegen-configured timezone.
if (value.has_parsed_timezone) {
const auto &pt = value.parsed_timezone;
if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) {
time::ParsedTimezone tz{};
tz.std_offset_seconds = pt.std_offset_seconds;
tz.dst_offset_seconds = pt.dst_offset_seconds;
tz.dst_start.time_seconds = pt.dst_start.time_seconds;
tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
tz.dst_end.time_seconds = pt.dst_end.time_seconds;
tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
time::set_global_tz(tz);
} else {
homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size());
}
time::ParsedTimezone tz{};
tz.std_offset_seconds = pt.std_offset_seconds;
tz.dst_offset_seconds = pt.dst_offset_seconds;
tz.dst_start.time_seconds = pt.dst_start.time_seconds;
tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
tz.dst_end.time_seconds = pt.dst_end.time_seconds;
tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
time::set_global_tz(tz);
}
#endif
}
@@ -2130,7 +2127,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
}
#endif
psk_t psk{};
noise::psk_t psk{};
if (msg.key_len == 0) {
if (this->parent_->clear_noise_psk(true)) {
resp.success = true;
@@ -2139,7 +2136,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
}
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length");
} else if (APINoiseContext::is_all_zeros(psk)) {
} else if (noise::NoiseContext::is_all_zeros(psk)) {
// Accepting the reserved provisioning PSK would report success without
// enabling encryption (or silently clear an existing key)
ESP_LOGW(TAG, "Rejecting all-zero encryption key");
+4 -4
View File
@@ -53,11 +53,11 @@ void log_dropped_message(const char *tag, int line, const LogString *what);
// Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending
static constexpr size_t MAX_INITIAL_PER_BATCH = 34;
// Deferred batch size cap during initial state/info sync
static constexpr size_t MAX_INITIAL_BATCH_SIZE = 34;
// Verify MAX_MESSAGES_PER_BATCH (defined in api_frame_helper.h) can hold the initial batch
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_BATCH_SIZE,
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_BATCH_SIZE");
#ifdef USE_BENCHMARK
class APIConnection;
+1 -1
View File
@@ -36,7 +36,7 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and oth
static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
// Maximum number of messages to batch in a single write operation
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
// Must be >= MAX_INITIAL_BATCH_SIZE in api_connection.h (enforced by static_assert there)
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
+55 -155
View File
@@ -2,9 +2,9 @@
#ifdef USE_API
#ifdef USE_API_NOISE
#include "api_connection.h" // For ClientInfo struct
#include "esphome/components/noise/noise.h"
#include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "proto.h"
@@ -17,6 +17,14 @@
namespace esphome::api {
using noise::noise_err_to_logstr;
// api_frame_helper.h keeps its own MAX_HANDSHAKE_SIZE because that header is
// also compiled in plaintext-only builds without the noise component; keep
// the two definitions from drifting apart.
static_assert(MAX_HANDSHAKE_SIZE == noise::MAX_HANDSHAKE_SIZE,
"api and noise component handshake size limits must match");
static const char *const TAG = "api.noise";
#ifdef USE_ESP8266
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
@@ -51,45 +59,6 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#endif
/// Convert a noise error code to a readable error
const LogString *noise_err_to_logstr(int err) {
if (err == NOISE_ERROR_NO_MEMORY)
return LOG_STR("NO_MEMORY");
if (err == NOISE_ERROR_UNKNOWN_ID)
return LOG_STR("UNKNOWN_ID");
if (err == NOISE_ERROR_UNKNOWN_NAME)
return LOG_STR("UNKNOWN_NAME");
if (err == NOISE_ERROR_MAC_FAILURE)
return LOG_STR("MAC_FAILURE");
if (err == NOISE_ERROR_NOT_APPLICABLE)
return LOG_STR("NOT_APPLICABLE");
if (err == NOISE_ERROR_SYSTEM)
return LOG_STR("SYSTEM");
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
return LOG_STR("REMOTE_KEY_REQUIRED");
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
return LOG_STR("LOCAL_KEY_REQUIRED");
if (err == NOISE_ERROR_PSK_REQUIRED)
return LOG_STR("PSK_REQUIRED");
if (err == NOISE_ERROR_INVALID_LENGTH)
return LOG_STR("INVALID_LENGTH");
if (err == NOISE_ERROR_INVALID_PARAM)
return LOG_STR("INVALID_PARAM");
if (err == NOISE_ERROR_INVALID_STATE)
return LOG_STR("INVALID_STATE");
if (err == NOISE_ERROR_INVALID_NONCE)
return LOG_STR("INVALID_NONCE");
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
return LOG_STR("INVALID_PRIVATE_KEY");
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
return LOG_STR("INVALID_PUBLIC_KEY");
if (err == NOISE_ERROR_INVALID_FORMAT)
return LOG_STR("INVALID_FORMAT");
if (err == NOISE_ERROR_INVALID_SIGNATURE)
return LOG_STR("INVALID_SIGNATURE");
return LOG_STR("UNKNOWN");
}
/// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() {
APIError err = init_common_();
@@ -194,9 +163,9 @@ APIError APINoiseFrameHelper::loop() {
*/
APIError APINoiseFrameHelper::try_read_frame_() {
// read header
if (rx_header_buf_len_ < 3) {
if (rx_header_buf_len_ < noise::FRAME_HEADER_SIZE) {
// no header information yet
uint8_t to_read = 3 - rx_header_buf_len_;
uint8_t to_read = static_cast<uint8_t>(noise::FRAME_HEADER_SIZE) - rx_header_buf_len_;
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
@@ -208,7 +177,7 @@ APIError APINoiseFrameHelper::try_read_frame_() {
return APIError::WOULD_BLOCK;
}
if (rx_header_buf_[0] != 0x01) {
if (rx_header_buf_[0] != noise::FRAME_INDICATOR) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
@@ -348,15 +317,15 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
return APIError::OK;
}
APIError APINoiseFrameHelper::state_action_handshake_() {
int action = noise_handshakestate_get_action(this->handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ) {
return this->state_action_handshake_read_();
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
} else if (action == noise::NoiseResponderHandshake::Action::ACTION_WRITE) {
return this->state_action_handshake_write_();
}
// bad state for action
this->state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
HELPER_LOG("Bad action for handshake: %d", (int) action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
APIError APINoiseFrameHelper::state_action_handshake_read_() {
@@ -368,20 +337,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
if (this->rx_buf_.empty()) {
this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (this->rx_buf_[0] != 0x00) {
} else if (this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) {
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
if (err != 0) {
// Special handling for MAC failure
this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
: LOG_STR("Handshake error"));
this->send_explicit_handshake_reject_(noise::reject_reason_for(err));
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
APIError::HANDSHAKESTATE_READ_FAILED);
}
@@ -390,18 +355,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
}
APIError APINoiseFrameHelper::state_action_handshake_write_() {
uint8_t buffer[65];
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
size_t msg_len = 0;
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
int err = this->handshake_.write_message(buffer + 1, sizeof(buffer) - 1, msg_len);
APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
APIError::HANDSHAKESTATE_WRITE_FAILED);
if (aerr != APIError::OK)
return aerr;
buffer[0] = 0x00; // success
buffer[0] = noise::HANDSHAKE_STATUS_OK;
aerr = this->write_frame_(buffer, mbuf.size + 1);
aerr = this->write_frame_(buffer, msg_len + 1);
if (aerr != APIError::OK)
return aerr;
return this->check_handshake_finished_();
@@ -409,33 +372,22 @@ APIError APINoiseFrameHelper::state_action_handshake_write_() {
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
uint8_t data[32];
data[0] = 0x01; // failure
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) {
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
// Normal memory access
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) {
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(data + 1, reason_str, reason_len);
}
#endif
size_t data_size = reason_len + 1;
static_assert(sizeof(data) >= noise::MAC_FAILURE_PAYLOAD_SIZE,
"reject buffer must fit the MAC failure wire contract");
size_t data_size = noise::format_reject_payload(data, sizeof(data), reason);
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data, data_size);
state_ = orig_state;
APIError aerr = write_frame_(data, data_size);
if (aerr != APIError::OK) {
// Best effort; the reject reason is a diagnosis aid, not a protocol step
ESP_LOGW(TAG, "Sending handshake reject failed: %d", (int) aerr);
}
if (state_ == State::EXPLICIT_REJECT) {
// write_frame_ may have moved the state to FAILED; keep that decision
state_ = orig_state;
}
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr = this->check_data_state_();
@@ -492,12 +444,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
// Returns APIError::OK on success.
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
uint16_t &encrypted_len_out) {
// Write noise header
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
// The noise frame header is written after encryption, when the size is known
// Write message header (to be encrypted)
constexpr uint8_t msg_offset = 3;
constexpr uint8_t msg_offset = noise::FRAME_HEADER_SIZE;
buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte
@@ -515,11 +465,10 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_
if (aerr != APIError::OK)
return aerr;
// Fill in the encrypted size
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Fill in the frame header now that the encrypted size is known
noise::write_frame_header(buf_start, static_cast<uint16_t>(mbuf.size));
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data
encrypted_len_out = static_cast<uint16_t>(noise::FRAME_HEADER_SIZE + mbuf.size);
return APIError::OK;
}
@@ -568,21 +517,19 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
uint8_t header[3];
header[0] = 0x01; // indicator
header[1] = (uint8_t) (len >> 8);
header[2] = (uint8_t) len;
uint8_t header[noise::FRAME_HEADER_SIZE];
noise::write_frame_header(header, len);
if (len == 0) {
return this->write_raw_buf_(header, 3);
return this->write_raw_buf_(header, noise::FRAME_HEADER_SIZE);
}
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = 3;
iov[0].iov_len = noise::FRAME_HEADER_SIZE;
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return this->write_raw_iov_(iov, 2, 3 + len);
return this->write_raw_iov_(iov, 2, noise::FRAME_HEADER_SIZE + len);
}
/** Initiate the data structures for the handshake.
@@ -590,45 +537,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
* @return 0 on success, -1 on error (check errno)
*/
APIError APINoiseFrameHelper::init_handshake_() {
int err;
// Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack:
// noise_handshakestate_new_by_id copies it, so a member would waste
// 104 bytes per connection, and a static const would sit in RAM on
// ESP8266 (.rodata is DRAM there).
const NoiseProtocolId nid = {
.prefix_id = NOISE_PREFIX_STANDARD,
.pattern_id = NOISE_PATTERN_NN,
.modifier_ids = {NOISE_MODIFIER_PSK0},
.dh_id = NOISE_DH_CURVE25519,
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE,
};
err = noise_handshakestate_new_by_id(&handshake_, &nid, NOISE_ROLE_RESPONDER);
APIError aerr =
handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED);
int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size());
APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
const auto &psk = this->ctx_.get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
// init copies the prologue into the handshakestate, so we can get rid of it now
prologue_.release();
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
return APIError::OK;
}
@@ -637,15 +551,17 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
assert(state_ == State::HANDSHAKE);
#endif
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ ||
action == noise::NoiseResponderHandshake::Action::ACTION_WRITE)
return APIError::OK;
if (action != NOISE_ACTION_SPLIT) {
if (action != noise::NoiseResponderHandshake::Action::ACTION_SPLIT) {
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
HELPER_LOG("Bad action for handshake: %d", (int) action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
// split() also frees the handshake state
int err = this->handshake_.split(send_cipher_, recv_cipher_);
APIError aerr =
handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED);
if (aerr != APIError::OK)
@@ -654,17 +570,11 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
state_ = State::DATA;
return APIError::OK;
}
APINoiseFrameHelper::~APINoiseFrameHelper() {
if (handshake_ != nullptr) {
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
}
if (send_cipher_ != nullptr) {
noise_cipherstate_free(send_cipher_);
send_cipher_ = nullptr;
@@ -675,16 +585,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
}
}
extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) {
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
arch_restart();
}
}
}
} // namespace esphome::api
#endif // USE_API_NOISE
#endif // USE_API
@@ -3,7 +3,7 @@
#ifdef USE_API
#ifdef USE_API_NOISE
#include "noise/protocol.h"
#include "api_noise_context.h"
#include "esphome/components/noise/noise_handshake.h"
namespace esphome::api {
@@ -14,9 +14,9 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len
static constexpr uint8_t HEADER_PADDING = noise::FRAME_HEADER_SIZE + 2 + 2; // frame header + type + data_len
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, noise::NoiseContext &ctx)
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
frame_header_padding_ = HEADER_PADDING;
}
@@ -52,13 +52,13 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError handle_handshake_frame_error_(APIError aerr);
APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err);
// Pointers first (4 bytes each)
NoiseHandshakeState *handshake_{nullptr};
// Pointers first (4 bytes each; the handshake wrapper holds one pointer)
noise::NoiseResponderHandshake handshake_;
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_;
noise::NoiseContext &ctx_;
// Buffer for noise handshake prologue (released after handshake)
APIBuffer prologue_;
@@ -67,7 +67,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
uint8_t rx_header_buf_[noise::FRAME_HEADER_SIZE];
uint8_t rx_header_buf_len_ = 0;
// 4 bytes total, no padding
};
@@ -1,37 +0,0 @@
#pragma once
#include <array>
#include <cstdint>
#include "esphome/core/defines.h"
namespace esphome::api {
#ifdef USE_API_NOISE
using psk_t = std::array<uint8_t, 32>;
class APINoiseContext {
public:
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
// doubles as the well-known provisioning PSK that unprovisioned devices
// accept for Noise handshakes (passive-sniffing protection only, no
// authentication). It is never a valid real key.
static bool is_all_zeros(const psk_t &psk) {
uint8_t acc = 0;
for (uint8_t b : psk) {
acc |= b;
}
return acc == 0;
}
void set_psk(psk_t psk) {
this->psk_ = psk;
this->has_psk_ = !is_all_zeros(psk);
}
const psk_t &get_psk() const { return this->psk_; }
bool has_psk() const { return this->has_psk_; }
protected:
psk_t psk_{};
bool has_psk_{false};
};
#endif // USE_API_NOISE
} // namespace esphome::api
+6
View File
@@ -116,4 +116,10 @@ extend google.protobuf.FieldOptions {
// the per-byte loop when the upper bits are non-zero (the common case
// for real MAC addresses, since OUIs occupy the top 24 bits).
optional bool mac_address = 50019 [default=false];
// track_presence: Track whether this message-typed field was present on the wire.
// Generates a `bool has_<field>{false};` member on the decoding side that is set
// to true when the field arrives, so an all-default submessage can be told apart
// from an absent one (e.g. a UTC ParsedTimezone, which is all zeros).
optional bool track_presence = 50020 [default=false];
}
+1 -4
View File
@@ -1249,12 +1249,9 @@ bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value
}
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->timezone = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
break;
}
case 3:
value.decode_to_message(this->parsed_timezone);
this->has_parsed_timezone = true;
break;
default:
return false;
+2 -2
View File
@@ -1283,13 +1283,13 @@ class ParsedTimezone final : public ProtoDecodableMessage {
class GetTimeResponse final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 37;
static constexpr uint8_t ESTIMATED_SIZE = 31;
static constexpr uint8_t ESTIMATED_SIZE = 22;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("get_time_response"); }
#endif
uint32_t epoch_seconds{0};
StringRef timezone{};
ParsedTimezone parsed_timezone{};
bool has_parsed_timezone{false};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
+1 -1
View File
@@ -1468,7 +1468,7 @@ const char *ParsedTimezone::dump_to(DumpBuffer &out) const {
const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("GetTimeResponse"));
dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds);
dump_field(out, ESPHOME_PSTR("timezone"), this->timezone);
dump_field(out, ESPHOME_PSTR("has_parsed_timezone"), this->has_parsed_timezone);
out.append(2, ' ').append_p(ESPHOME_PSTR("parsed_timezone")).append(": ");
this->parsed_timezone.dump_to(out);
out.append("\n");
+1 -1
View File
@@ -588,7 +588,7 @@ bool APIServer::load_and_apply_noise_psk_() {
return true;
}
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) {
#ifdef USE_API_NOISE_PSK_FROM_YAML
// When PSK is set from YAML, this function should never be called
// but if it is, reject the change
+9 -6
View File
@@ -5,7 +5,10 @@
#include "api_buffer.h"
// Must precede clients_ so APIConnection is complete for default_delete (libc++).
#include "api_connection.h"
#include "api_noise_context.h"
#ifdef USE_API_NOISE
// Only present in the build when the noise component is loaded
#include "esphome/components/noise/noise.h"
#endif
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "esphome/components/socket/socket.h"
@@ -37,7 +40,7 @@ class UserServiceDescriptor;
#ifdef USE_API_NOISE
struct SavedNoisePsk {
psk_t psk;
noise::psk_t psk;
} PACKED; // NOLINT
#endif
@@ -73,10 +76,10 @@ class APIServer final : public Component,
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
#ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
bool save_noise_psk(noise::psk_t psk, bool make_active = true);
bool clear_noise_psk(bool make_active = true);
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; }
#endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn);
@@ -354,7 +357,7 @@ class APIServer final : public Component,
#endif
#ifdef USE_API_NOISE
APINoiseContext noise_ctx_;
noise::NoiseContext noise_ctx_;
ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE
};
+9 -1
View File
@@ -95,9 +95,17 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
#ifdef USE_API_USER_DEFINED_ACTIONS
// Yield after every Nth service; bounds direct (non-batched) writes per loop pass
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response();
return this->client_->send_message(resp);
if (!this->client_->send_message(resp))
return false;
// at_ is this service's index
if ((this->at_ + 1) % SERVICE_YIELD_INTERVAL == 0)
this->yield_after_step_();
return true;
}
#endif
+3 -2
View File
@@ -7,6 +7,7 @@ from esphome.const import (
DEVICE_CLASS_AQI,
STATE_CLASS_MEASUREMENT,
)
from esphome.types import ConfigType
from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, CONF_EXTENDED_RANGE, aqi_ns
@@ -16,7 +17,7 @@ DEPENDENCIES = ["sensor"]
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
def _validate_extended_range(config):
def _validate_extended_range(config: ConfigType) -> ConfigType:
if CONF_EXTENDED_RANGE in config and config[CONF_CALCULATION_TYPE] == "CAQI":
raise cv.Invalid(
f"'{CONF_EXTENDED_RANGE}' is not supported with 'calculation_type: CAQI'. "
@@ -48,7 +49,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import as3935, i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
AUTO_LOAD = ["as3935"]
DEPENDENCIES = ["i2c"]
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await as3935.setup_as3935(var, config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import as3935, spi
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
AUTO_LOAD = ["as3935"]
DEPENDENCIES = ["spi"]
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await as3935.setup_as3935(var, config)
await spi.register_spi_device(var, config)
+2 -1
View File
@@ -9,6 +9,7 @@ from esphome.const import (
ICON_BRIGHTNESS_5,
STATE_CLASS_MEASUREMENT,
)
from esphome.types import ConfigType
CODEOWNERS = ["@mrgnr"]
DEPENDENCIES = ["i2c"]
@@ -96,7 +97,7 @@ SENSORS = {
}
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"]
@@ -25,7 +26,7 @@ CONFIG_SCHEMA = cv.Schema({})
@coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
if CORE.is_esp32:
# https://github.com/ESP32Async/AsyncTCP
from esphome.components.esp32 import add_idf_component
@@ -21,6 +21,7 @@ from esphome.const import (
UNIT_PERCENT,
UNIT_VOLT,
)
from esphome.types import ConfigType
CODEOWNERS = ["@ahpohl"]
@@ -77,7 +78,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await ble_device_base.register_ble_device(var, config)
+2 -1
View File
@@ -30,6 +30,7 @@ from esphome.const import (
UNIT_WATT,
UNIT_WATT_HOURS,
)
from esphome.types import ConfigType
CONF_METER_CONSTANT = "meter_constant"
CONF_PL_CONST = "pl_const"
@@ -123,7 +124,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
@@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from esphome.types import ConfigType
from .. import axs15231_ns
@@ -25,7 +26,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -20,6 +20,7 @@ from esphome.const import (
UNIT_PERCENT,
UNIT_VOLT,
)
from esphome.types import ConfigType
CODEOWNERS = ["@rbaron"]
@@ -74,7 +75,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await ble_device_base.register_ble_device(var, config)
+2 -1
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import climate_ir
from esphome.types import ConfigType
AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@bazuchan"]
@@ -10,5 +11,5 @@ BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
await climate_ir.new_climate_ir(config)
+2 -1
View File
@@ -12,6 +12,7 @@ from esphome.const import (
CONF_IDLE_ACTION,
CONF_SENSOR,
)
from esphome.types import ConfigType
bang_bang_ns = cg.esphome_ns.namespace("bang_bang")
BangBangClimate = bang_bang_ns.class_("BangBangClimate", climate.Climate, cg.Component)
@@ -41,7 +42,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await climate.new_climate(config)
await cg.register_component(var, config)
@@ -56,7 +56,7 @@ SUPPORTED_PINS = {
}
def _validate_pin(value):
def _validate_pin(value: int) -> int:
family = libretiny.get_libretiny_family()
if family not in SUPPORTED_PINS:
raise cv.Invalid(f"Chip family {family} is not supported.")
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT, UNIT_LUX
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@OttoWinter"]
@@ -25,7 +26,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -6,6 +6,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@B48D81EFCC"]
@@ -28,7 +29,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import fan, output
import esphome.config_validation as cv
from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT
from esphome.types import ConfigType
from .. import binary_ns
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await fan.new_fan(config)
await cg.register_component(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import light, output
import esphome.config_validation as cv
from esphome.const import CONF_OUTPUT, CONF_OUTPUT_ID
from esphome.types import ConfigType
from .. import binary_ns
@@ -15,7 +16,7 @@ CONFIG_SCHEMA = light.BINARY_LIGHT_SCHEMA.extend(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(var, config)
@@ -10,6 +10,7 @@ from esphome.const import (
CONF_VALUE,
ICON_CHECK_CIRCLE_OUTLINE,
)
from esphome.types import ConfigType
DEPENDENCIES = ["binary_sensor"]
@@ -82,7 +83,7 @@ CONFIG_SCHEMA = cv.typed_schema(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
+5 -3
View File
@@ -28,6 +28,8 @@ from esphome.components.libretiny.const import (
LibreTinyComponent,
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
from .boards import BK72XX_BOARD_PINS, BK72XX_BOARDS
@@ -45,7 +47,7 @@ COMPONENT_DATA = LibreTinyComponent(
)
def _set_core_data(config):
def _set_core_data(config: ConfigType) -> ConfigType:
CORE.data[KEY_LIBRETINY] = {}
CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA
return config
@@ -62,12 +64,12 @@ PIN_SCHEMA = libretiny.gpio.BASE_PIN_SCHEMA
CONFIG_SCHEMA.prepend_extra(_set_core_data)
async def to_code(config):
async def to_code(config: ConfigType) -> MockObj:
return await libretiny.component_to_code(config)
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
async def pin_to_code(config):
async def pin_to_code(config: ConfigType) -> MockObj:
return await libretiny.gpio.component_pin_to_code(config)
+2 -1
View File
@@ -15,6 +15,7 @@ from esphome.const import (
UNIT_VOLT,
UNIT_WATT,
)
from esphome.types import ConfigType
DEPENDENCIES = ["uart"]
@@ -88,7 +89,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
+2 -1
View File
@@ -24,6 +24,7 @@ from esphome.const import (
UNIT_VOLT,
UNIT_WATT,
)
from esphome.types import ConfigType
CONF_CURRENT_REFERENCE = "current_reference"
CONF_ENERGY_REFERENCE = "energy_reference"
@@ -95,7 +96,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
+27 -16
View File
@@ -206,32 +206,36 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType:
interval = config[CONF_INTERVAL]
window = config[CONF_WINDOW]
if window > interval:
raise cv.Invalid(
f"Scan window ({window}) needs to be smaller than scan interval ({interval})"
)
# Labels are reused in every error below; the optional one names its key.
windows = [("Scan window", window)]
if (connection_window := config.get(CONF_CONNECTION_SCAN_WINDOW)) is not None:
windows.append((CONF_CONNECTION_SCAN_WINDOW, connection_window))
for name, value in windows:
if value > interval:
raise cv.Invalid(
f"{name} ({value}) needs to be smaller than scan interval ({interval})"
)
# BLE scan interval/window are programmed in 0.625 ms units as a 16-bit value; the
# controller only accepts 2.5 ms .. 10240 ms (0x0004 .. 0x4000). Reject out-of-range
# values here instead of letting the unit conversion silently overflow.
for name, value in (("interval", interval), ("window", window)):
for name, value in (("Scan interval", interval), *windows):
if value.total_microseconds < 2500 or value.total_microseconds > 10_240_000:
raise cv.Invalid(
f"Scan {name} ({value}) must be between 2.5 ms and 10240 ms"
)
raise cv.Invalid(f"{name} ({value}) must be between 2.5 ms and 10240 ms")
# Validate what actually reaches the controller: both values are truncated to
# whole 0.625 ms units, so a window/interval pair that differs by less than one
# unit collapses to the same value — silently programming a 100 % duty cycle
# (radio permanently on) from a config that asked for less.
interval_units = to_ble_units(interval)
window_units = to_ble_units(window)
if window_units == interval_units and window < interval:
raise cv.Invalid(
f"Scan window ({window}) and interval ({interval}) both truncate to "
f"{interval_units} x 0.625 ms, which the controller scans at a 100 % duty "
f"cycle. Separate them by at least 0.625 ms."
)
for name, value in windows:
if to_ble_units(value) == interval_units and value < interval:
raise cv.Invalid(
f"{name} ({value}) and interval ({interval}) both truncate to "
f"{interval_units} x 0.625 ms, which the controller scans at a 100 % duty "
f"cycle. Separate them by at least 0.625 ms."
)
if interval.total_microseconds * 3 > duration.total_microseconds:
raise cv.Invalid(
@@ -247,11 +251,14 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType:
# their own; also the fallback for esp32's conditional default.
DEFAULT_SCAN_WINDOW = "30ms"
CONF_CONNECTION_SCAN_WINDOW = "connection_scan_window"
def scan_parameters_schema(
interval_default: str,
*,
window_default: str | Callable[[], TimePeriod] = DEFAULT_SCAN_WINDOW,
connection_window: bool = False,
) -> cv.All:
"""Build the scan_parameters value schema shared by all BLE trackers.
@@ -263,7 +270,9 @@ def scan_parameters_schema(
can adjust it once sibling keys are resolved). The `active` option
(default on) is unconditional: active scanning is part of the tracker
contract every current proxy client assumes it, so a passive-only
tracker must not share this schema.
tracker must not share this schema. connection_window opts in to the
`connection_scan_window` option for trackers that can fall back to a
smaller window while a GATT connection is active.
"""
schema = {
cv.Optional(CONF_DURATION, default="5min"): cv.positive_time_period_seconds,
@@ -272,6 +281,8 @@ def scan_parameters_schema(
cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean,
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
}
if connection_window:
schema[cv.Optional(CONF_CONNECTION_SCAN_WINDOW)] = cv.positive_time_period
return cv.All(cv.Schema(schema), validate_scan_parameters)
@@ -1,5 +1,6 @@
"""Shared codegen for the neutral BLE advertisement triggers (automation.h)."""
from collections.abc import Callable
from typing import Any
from esphome import automation
@@ -52,7 +53,7 @@ _UUID_WIDTHS = {
def uuid_trigger_schema(
trigger_class: MockObjClass, extra: dict[Any, Any] | None = None
):
) -> Callable[[Any], Any]:
"""Schema for a UUID-filtered trigger — pairs with uuid_trigger_to_code().
`extra` carries the required UUID key (a cv marker, so a dict rather than
@@ -68,7 +69,7 @@ def uuid_trigger_schema(
)
def advertise_trigger_schema(trigger_class: MockObjClass):
def advertise_trigger_schema(trigger_class: MockObjClass) -> Callable[[Any], Any]:
"""on_ble_advertise schema: multi-mac list filter, unlike the single-mac
uuid_trigger_schema() pairs with advertise_trigger_to_code()."""
return automation.validate_automation(
@@ -79,7 +80,7 @@ def advertise_trigger_schema(trigger_class: MockObjClass):
)
def scan_end_trigger_schema(trigger_class: MockObjClass):
def scan_end_trigger_schema(trigger_class: MockObjClass) -> Callable[[Any], Any]:
"""on_scan_end schema: id only — pairs with scan_end_trigger_to_code()."""
return automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class)}
@@ -10,6 +10,7 @@ from esphome.const import (
CONF_SERVICE_UUID,
CONF_TIMEOUT,
)
from esphome.types import ConfigType
CONF_IRK = "irk"
@@ -24,7 +25,7 @@ BLEPresenceDevice = ble_presence_ns.class_(
)
def _validate(config):
def _validate(config: ConfigType) -> ConfigType:
if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config:
raise cv.Invalid("iBeacon major identifier requires iBeacon UUID")
if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config:
@@ -58,7 +59,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
await ble_device_base.register_ble_device(var, config)
+3 -2
View File
@@ -11,6 +11,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_DECIBEL_MILLIWATT,
)
from esphome.types import ConfigType
CONF_IRK = "irk"
@@ -22,7 +23,7 @@ BLERSSISensor = ble_rssi_ns.class_(
)
def _validate(config):
def _validate(config: ConfigType) -> ConfigType:
if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config:
raise cv.Invalid("iBeacon major identifier requires iBeacon UUID")
if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config:
@@ -58,7 +59,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await ble_device_base.register_ble_device(var, config)
@@ -1,6 +1,7 @@
import esphome.codegen as cg
from esphome.components import ble_device_base, text_sensor
import esphome.config_validation as cv
from esphome.types import ConfigType
AUTO_LOAD = ["ble_device_base"]
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await text_sensor.new_text_sensor(config)
await cg.register_component(var, config)
await ble_device_base.register_ble_device(var, config)
@@ -275,10 +275,15 @@ void BluetoothConnection::send_ack_(PendingAck kind, uint16_t handle, conn_err_t
if (this->try_send_ack_(kind, handle, error))
return;
// Report a newly owed reply and a displaced one; displacing is the case
// that loses a reply. Re-refusing the same one stays quiet.
// that loses a reply. Re-refusing the same one stays quiet, and so does a
// fresh deferral for the handle already warned about: a congested bulk
// transfer re-asks the same handle every cycle and each ack would warn.
if (!this->has_pending_ack_()) {
ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X deferred, TCP buffer full", this->connection_index_,
this->address_str_, handle);
if (!this->ack_deferred_warned_ || this->pending_ack_handle_ != handle) {
ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X deferred, TCP buffer full", this->connection_index_,
this->address_str_, handle);
this->ack_deferred_warned_ = true;
}
} else if (this->pending_ack_handle_ != handle || this->pending_ack_ != kind) {
ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X dropped for handle 0x%04X", this->connection_index_,
this->address_str_, this->pending_ack_handle_, handle);
@@ -365,8 +370,16 @@ void BluetoothConnection::on_notify_data(uint16_t handle, const uint8_t *data, u
resp.set_data(data, len);
if (!api_connection->send_message(resp)) {
// Not latched, same reason as the read reply. Notify data is lossy: the
// peripheral will not resend it.
ESP_LOGW(TAG, "[%d] [%s] Failed to send notify data response", this->connection_index_, this->address_str_);
// peripheral will not resend it. Warn on the first drop only; a congested
// link drops a whole stream and one line per notify floods the log.
if (!this->notify_drop_warned_) {
ESP_LOGW(TAG, "[%d] [%s] Failed to send notify data response, handle 0x%04X", this->connection_index_,
this->address_str_, handle);
this->notify_drop_warned_ = true;
} else {
ESP_LOGV(TAG, "[%d] [%s] Failed to send notify data response, handle 0x%04X", this->connection_index_,
this->address_str_, handle);
}
}
}
@@ -164,6 +164,8 @@ class BluetoothConnection final : public ble_device_base::GattClientListener {
this->pending_ack_ = PendingAck::PENDING_ACK_NONE;
this->batch_stalled_ = false;
this->connected_reply_owed_ = false;
this->ack_deferred_warned_ = false;
this->notify_drop_warned_ = false;
}
/// Sole construction site for these replies, shared by send and retry.
bool try_send_ack_(PendingAck kind, uint16_t handle, conn_err_t error);
@@ -238,7 +240,7 @@ class BluetoothConnection final : public ble_device_base::GattClientListener {
// Group 5: bit-packed tail. The first two bytes were already full, so the
// first added bit forced a third and took the 8-aligned object 48 -> 56;
// the handle, error and retry counter ride in that padding. Four bitfield
// the handle, error and retry counter ride in that padding. Two bitfield
// bits left; another byte-sized member costs 8 per slot.
static_assert(static_cast<uint8_t>(ClientState::ESTABLISHED) < (1 << 3), "state_ bitfield too narrow");
static_assert(static_cast<uint8_t>(ConnectionType::V3_WITHOUT_CACHE) < (1 << 2),
@@ -258,6 +260,11 @@ class BluetoothConnection final : public ble_device_base::GattClientListener {
bool batch_stalled_ : 1 {false};
/// An owed connected=true reply; the proxy's paced drain re-offers it.
bool connected_reply_owed_ : 1 {false};
/// Set once the deferred warn fired; with an unchanged pending_ack_handle_
/// it keeps re-deferrals of the same handle quiet (see send_ack_).
bool ack_deferred_warned_ : 1 {false};
/// Set on the first dropped notify; later drops log at verbose only.
bool notify_drop_warned_ : 1 {false};
// Plain byte after the bitfields: takes the padding byte instead of
// straddling pending_ack_'s storage unit and growing the object.
static_assert(PENDING_ACK_RETRY_LIMIT <= 0xFF, "retry counter too narrow");
@@ -92,7 +92,7 @@ def _esp32_config_schema() -> cv.All:
CONNECTION_SCHEMA = bluetooth_connection.hub_connection_schema(PLATFORM_ESP32)
def validate_connections(config):
def validate_connections(config: ConfigType) -> ConfigType:
if CONF_CONNECTIONS in config:
if not config[CONF_ACTIVE]:
raise cv.Invalid(
+3 -1
View File
@@ -16,6 +16,8 @@ from esphome.const import (
UNIT_HECTOPASCAL,
UNIT_PERCENT,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
@@ -84,7 +86,7 @@ CONFIG_SCHEMA_BASE = cv.Schema(
).extend(cv.polling_component_schema("60s"))
async def to_code_base(config):
async def to_code_base(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+2 -1
View File
@@ -1,6 +1,7 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.types import ConfigType
from ..bme280_base import CONFIG_SCHEMA_BASE, to_code_base
@@ -17,6 +18,6 @@ CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
).extend({cv.GenerateID(): cv.declare_id(BME280I2CComponent)})
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -1,6 +1,7 @@
import esphome.codegen as cg
from esphome.components import spi
import esphome.config_validation as cv
from esphome.types import ConfigType
from ..bme280_base import CONFIG_SCHEMA_BASE, to_code_base
@@ -19,6 +20,6 @@ CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await to_code_base(config)
await spi.register_spi_device(var, config)
+2 -1
View File
@@ -22,6 +22,7 @@ from esphome.const import (
UNIT_OHM,
UNIT_PERCENT,
)
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
@@ -125,7 +126,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
@@ -6,6 +6,7 @@ from esphome.components.bme68x_bsec2 import (
to_code_base,
)
import esphome.config_validation as cv
from esphome.types import ConfigType
CODEOWNERS = ["@neffs", "@kbx81"]
@@ -29,6 +30,6 @@ CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
).extend(i2c.i2c_device_schema(0x76))
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -22,6 +22,7 @@ from esphome.const import (
UNIT_DEGREE_PER_SECOND,
UNIT_METER_PER_SECOND_SQUARED,
)
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
@@ -82,7 +83,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -8,6 +8,7 @@ from esphome.components.const import (
)
from esphome.components.motion import motion_schema, new_motion_component
import esphome.config_validation as cv
from esphome.types import ConfigType
from . import BMI270Component, bmi270_ns
@@ -79,7 +80,7 @@ CONFIG_SCHEMA = (
# Code generation
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await new_motion_component(config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -11,6 +11,7 @@ from esphome.const import (
UNIT_CELSIUS,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
from . import CONF_BMI270_ID, BMI270Component
@@ -30,7 +31,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
parent = await cg.get_variable(config[CONF_BMI270_ID])
data = MockObj("data")
+2 -1
View File
@@ -11,6 +11,7 @@ from esphome.const import (
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
)
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
@@ -42,7 +43,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+3 -1
View File
@@ -13,6 +13,8 @@ from esphome.const import (
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@ademuri"]
@@ -69,7 +71,7 @@ CONFIG_SCHEMA_BASE = cv.Schema(
).extend(cv.polling_component_schema("60s"))
async def to_code_base(config):
async def to_code_base(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+2 -1
View File
@@ -1,6 +1,7 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.types import ConfigType
from ..bmp280_base import CONFIG_SCHEMA_BASE, to_code_base
@@ -18,6 +19,6 @@ CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
).extend({cv.GenerateID(): cv.declare_id(BMP280I2CComponent)})
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -1,6 +1,7 @@
import esphome.codegen as cg
from esphome.components import spi
import esphome.config_validation as cv
from esphome.types import ConfigType
from ..bmp280_base import CONFIG_SCHEMA_BASE, to_code_base
@@ -18,6 +19,6 @@ CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
).extend({cv.GenerateID(): cv.declare_id(BMP280SPIComponent)})
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await to_code_base(config)
await spi.register_spi_device(var, config)
+3 -1
View File
@@ -13,6 +13,8 @@ from esphome.const import (
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@martgras", "@latonita"]
@@ -73,7 +75,7 @@ CONFIG_SCHEMA_BASE = cv.Schema(
).extend(cv.polling_component_schema("60s"))
async def to_code_base(config):
async def to_code_base(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+2 -1
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import i2c
from esphome.types import ConfigType
from ..bmp3xx_base import CONFIG_SCHEMA_BASE, cv, to_code_base
@@ -18,6 +19,6 @@ CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
).extend({cv.GenerateID(): cv.declare_id(BMP3XXI2CComponent)})
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)
+2 -1
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import spi
from esphome.types import ConfigType
from ..bmp3xx_base import CONFIG_SCHEMA_BASE, cv, to_code_base
@@ -18,6 +19,6 @@ CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await to_code_base(config)
await spi.register_spi_device(var, config)
+4 -2
View File
@@ -15,6 +15,8 @@ from esphome.const import (
UNIT_CELSIUS,
UNIT_PASCAL,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
@@ -47,7 +49,7 @@ IIR_FILTER_OPTIONS = {
BMP581Component = bmp581_ns.class_("BMP581Component", cg.PollingComponent)
def compute_measurement_conversion_time(config):
def compute_measurement_conversion_time(config: ConfigType) -> int:
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
@@ -132,7 +134,7 @@ CONFIG_SCHEMA_BASE = cv.Schema(
).extend(cv.polling_component_schema("60s"))
async def to_code_base(config):
async def to_code_base(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
+2 -1
View File
@@ -1,6 +1,7 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.types import ConfigType
from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base
@@ -18,6 +19,6 @@ CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
).extend({cv.GenerateID(): cv.declare_id(BMP581I2CComponent)})
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)
+3 -2
View File
@@ -4,6 +4,7 @@ import esphome.codegen as cg
from esphome.components import spi
from esphome.components.spi import CONF_SPI_MODE
import esphome.config_validation as cv
from esphome.types import ConfigType
from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base
@@ -28,7 +29,7 @@ BMP581SPIComponent = bmp581_ns.class_(
)
def check_spi_mode(config):
def check_spi_mode(config: ConfigType) -> ConfigType:
spi_mode = config.get(CONF_SPI_MODE)
if spi_mode not in VALID_SPI_MODES:
raise cv.Invalid("BMP581 only supports SPI mode 0 or mode 3")
@@ -43,6 +44,6 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await to_code_base(config)
await spi.register_spi_device(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ from esphome import pins
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_ID
from esphome.types import ConfigType
CODEOWNERS = ["@Cossid"]
MULTI_CONF = True
@@ -28,7 +29,7 @@ CONFIG_SCHEMA = cv.Schema(
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.types import ConfigType
from . import BP1658CJ
@@ -19,7 +20,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ from esphome import pins
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_ID
from esphome.types import ConfigType
CODEOWNERS = ["@Cossid"]
MULTI_CONF = True
@@ -19,7 +20,7 @@ CONFIG_SCHEMA = cv.Schema(
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_CURRENT, CONF_ID
from esphome.types import ConfigType
from . import BP5758D
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
+2 -1
View File
@@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_RESET_PIN
from esphome.types import ConfigType
CONF_TOUCH_THRESHOLD = "touch_threshold"
CONF_ALLOW_MULTIPLE_TOUCHES = "allow_multiple_touches"
@@ -32,7 +33,7 @@ CONFIG_SCHEMA = (
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD]))
cg.add(var.set_allow_multiple_touches(config[CONF_ALLOW_MULTIPLE_TOUCHES]))
+2 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL
from esphome.types import ConfigType
from . import CONF_CAP1188_ID, CAP1188Component, cap1188_ns
@@ -16,7 +17,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CAP1188Channel).extend(
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = await binary_sensor.new_binary_sensor(config)
hub = await cg.get_variable(config[CONF_CAP1188_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
@@ -93,7 +93,7 @@ FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
var = cg.new_Pvariable(config[CONF_ID], paren)
@@ -4,9 +4,9 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
#include "esphome/components/wifi/scan_list.h"
#include "esphome/components/wifi/wifi_component.h"
#include "captive_index.h"
#include "scan_list.h"
namespace esphome::captive_portal {
@@ -37,7 +37,7 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
const auto &results = wifi::global_wifi_component->get_scan_result();
for (const auto &scan : results) {
bool with_auth = false;
if (!should_show_scan_entry(results, scan, with_auth))
if (!wifi::should_show_scan_entry(results, scan, with_auth))
continue;
json_escape_into_buffer(escaped_ssid, scan.get_ssid());

Some files were not shown because too many files have changed in this diff Show More