Compare commits

...
Author SHA1 Message Date
J. Nick Koston 0094cee7b6 Merge remote-tracking branch 'origin/dev' into apply-entities-button 2026-10-21 08:40:54 -10:00
Jonathan Swoboda 35883270fe [ci] Ignore BUILD_ALWAYS external project byproducts in the idf.py check (#21291) 2026-10-21 08:35:58 -10:00
J. Nick Koston da37e89b9d [tests] Let the offline probe finish before checking that the continuous modbus poll stopped (#21293) 2026-10-21 08:35:38 -10:00
J. Nick Koston 32bbf55f2a [tests] Clear testing_mode after every test (#21292) 2026-10-21 08:35:22 -10:00
Kevin Ahrendt 16759c848f [i2s_audio] Convert audio to the configured slot width on the original ESP32 (#20893) 2026-10-21 14:25:38 -04:00
J. Nick Koston 244e419f82 [tests] Drop the timeout wrappers around the integration state helpers (#21245) 2026-10-21 08:04:00 -10:00
J. Nick Koston 04f53f0ab4 [tests] Fail the test when the integration state helpers time out (#21244) 2026-10-21 08:03:59 -10:00
9eef601f99 [network][socket] Allow disabling IPV4 (#14526)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-21 13:57:49 -04:00
J. Nick Koston 454dbe59db [tests] Speed up the modbus uart_mock integration tests (#21250) 2026-10-21 07:48:38 -10:00
J. Nick Koston 7c0fa49b00 [tests] Speed up the host OTA integration tests (#21248) 2026-10-21 13:29:58 -04:00
J. Nick KostonandJonathan Swoboda c9fe7451a6 [tests] Use wait_or_fail in the scheduler and loop integration tests (#21241)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-21 17:29:44 +00:00
J. Nick Koston 92320ed14e [tests] Use wait_or_fail in the uart_mock integration tests (#21247) 2026-10-21 13:28:32 -04:00
J. Nick Koston 601dcce0e4 [tests] Use require_entity and require_only_entity for the remaining entity lookups (#21243) 2026-10-21 13:20:50 -04:00
J. Nick Koston 98ab149c24 [dfrobot_sen0609] Use Apply buttons for the hub buttons 2026-10-20 17:08:45 -10:00
J. Nick Koston 49a8c1d4cc Merge remote-tracking branch 'origin/dev' into apply-entities-button 2026-10-19 07:52:24 -10:00
J. Nick Koston 39bfdecf75 Merge branch 'apply-entities' into apply-entities-button 2026-10-16 14:07:10 -10:00
J. Nick Koston 754a960ed1 [core] Declare apply locals right before their first use
A local declared at the top ran option_at before a publish placed before
the statement, which changed the call order and grew ld2412 by 16 bytes.
2026-10-16 14:06:17 -10:00
J. Nick Koston 4f1db5b3d1 Merge branch 'apply-entities' into apply-entities-button 2026-10-16 14:04:31 -10:00
J. Nick Koston f7a60f5859 [core] Give select apply statements an option local
Select apply statements can name the selected option text as option; the
local is declared only when a statement uses it.
2026-10-16 14:04:26 -10:00
J. Nick Koston d0d79d5c54 [tfluna] Use Apply buttons 2026-10-16 13:46:03 -10:00
J. Nick Koston ab2383e7c5 [seeed_mr60fda2] Use Apply buttons 2026-10-16 13:46:03 -10:00
J. Nick Koston def600ff6a [seeed_mr24hpc1] Use Apply buttons 2026-10-16 13:46:02 -10:00
J. Nick Koston f1dda7da41 [ld2450] Use Apply buttons 2026-10-16 13:46:02 -10:00
J. Nick Koston b6f101d5a6 [ld2420] Use Apply buttons 2026-10-16 13:46:01 -10:00
J. Nick Koston 7c6c0fb4bc [ld2412] Use Apply buttons 2026-10-16 13:46:01 -10:00
J. Nick Koston 75ac3f51b5 [ld2410] Use Apply buttons 2026-10-16 13:46:01 -10:00
J. Nick Koston d9e3f2ae3e [hoermann_hcp] Use Apply buttons 2026-10-16 13:46:01 -10:00
J. Nick Koston 5cc6f6a89d [haier] Use Apply buttons 2026-10-16 13:46:01 -10:00
J. Nick Koston 23bc50d229 [espectre] Use Apply buttons 2026-10-16 13:46:00 -10:00
J. Nick Koston 472b2281d1 [bq25186] Use Apply buttons 2026-10-16 13:46:00 -10:00
J. Nick Koston b8a0de91db [core] Simplify the Apply entity helpers
Fold the function cache into SubEntities, make each domain's APPLY_ENTITY public,
shorten the class and docstring comments and reuse the shared test hub.
2026-10-16 13:42:23 -10:00
J. Nick Koston 2937d81216 Mark the test recorders as intentional mutable globals 2026-10-16 12:20:50 -10:00
J. Nick Koston 05a4b256db Reject an unknown publish= value 2026-10-16 11:45:47 -10:00
J. Nick Koston d0427c36eb [core] Share the apply function naming with automations and check the Apply class at codegen 2026-10-16 11:04:36 -10:00
J. Nick Koston a990231342 [core] Add Apply button, switch, number and select entities for hub forwarders 2026-10-16 10:48:29 -10:00
203 changed files with 2197 additions and 1965 deletions

No files matched your search

+5 -23
View File
@@ -7,7 +7,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_DESCRIPTION, CONF_HOST
from esphome.components.logger import request_log_listener
from esphome.components.network import DOMAIN as NETWORK_DOMAIN
from esphome.components.network import require_ipv4, require_ipv6
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
# components and downstream consumers that import them from api
@@ -28,7 +28,6 @@ from esphome.const import (
CONF_DATA,
CONF_DATA_TEMPLATE,
CONF_DELAY,
CONF_ENABLE_IPV6,
CONF_ENCRYPTION,
CONF_EVENT,
CONF_ID,
@@ -52,7 +51,6 @@ from esphome.const import (
)
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.cpp_generator import Expression, MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.helpers import fnv1_hash
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigFragmentType, ConfigType
@@ -333,7 +331,10 @@ def _validate_outgoing_host(value: str) -> IPv4Address | IPv6Address:
"give the address without the '%' part"
)
if (mapped := address.ipv4_mapped) is not None:
return mapped
address = mapped
# The build must carry the host's address family for the dial to work
require = require_ipv6 if isinstance(address, IPv6Address) else require_ipv4
require("api.outgoing_connection")(value)
return address
@@ -479,31 +480,12 @@ def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
return strings
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
"""An IPv6 host can never be parsed, so never dialed, without IPv6."""
if (
(outgoing := config.get(CONF_OUTGOING_CONNECTION)) is None
or (host := outgoing.get(CONF_HOST)) is None
or host.version != 6
):
return config
network_conf = fv.full_config.get().get(NETWORK_DOMAIN) or {}
if not network_conf.get(CONF_ENABLE_IPV6):
raise cv.Invalid(
"outgoing_connection host is an IPv6 address but IPv6 is not "
"enabled; set 'network: enable_ipv6: true'",
path=[CONF_OUTGOING_CONNECTION, CONF_HOST],
)
return config
def _validate_wizard(config: ConfigType) -> ConfigType:
wizard.final_validate(config)
return config
FINAL_VALIDATE_SCHEMA = cv.All(
_validate_outgoing_host_ipv6,
_validate_wizard,
)
@@ -38,12 +38,14 @@ static bool peer_to_target(APIConnection *conn, SavedOutgoingTarget &target) {
const auto *bytes = reinterpret_cast<const uint8_t *>(&addr6->sin6_addr);
uint32_t prefix[3];
memcpy(prefix, bytes, sizeof(prefix));
#if USE_NETWORK_IPV4
// A dual-stack listener reports an IPv4 peer as ::ffff:a.b.c.d
if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == htonl(0xFFFFUL)) {
target.family = AF_INET;
memcpy(target.addr, bytes + sizeof(prefix), sizeof(struct in_addr));
return true;
}
#endif
// A link-local target is only reachable through the interface it came in
// on. Device platforms number interfaces from one; a host build can hand
// out an index too large to store, and a truncated one dials the wrong
@@ -57,6 +59,7 @@ static bool peer_to_target(APIConnection *conn, SavedOutgoingTarget &target) {
return true;
}
#endif
#if USE_NETWORK_IPV4
if (family != AF_INET) {
return false;
}
@@ -64,6 +67,9 @@ static bool peer_to_target(APIConnection *conn, SavedOutgoingTarget &target) {
target.family = AF_INET;
memcpy(target.addr, &addr4->sin_addr, sizeof(addr4->sin_addr));
return true;
#else
return false;
#endif
}
#endif
@@ -85,6 +91,7 @@ socklen_t OutgoingConnectionManager::target_sockaddr_(struct sockaddr_storage *a
return sizeof(*addr6);
}
#endif
#if USE_NETWORK_IPV4
if (this->saved_.family != AF_INET) {
return 0;
}
@@ -94,6 +101,9 @@ socklen_t OutgoingConnectionManager::target_sockaddr_(struct sockaddr_storage *a
addr4->sin_port = htons(API_OUTGOING_CONNECTION_PORT);
memcpy(&addr4->sin_addr, this->saved_.addr, sizeof(addr4->sin_addr));
return sizeof(*addr4);
#else
return 0;
#endif
#endif
}
@@ -1,25 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BUTTON
#include "bq25186.h"
#include "esphome/components/button/button.h"
namespace esphome::bq25186 {
/// Writes one SHIP_RST field when pressed
class BQ25186Button final : public button::Button, public Parented<BQ25186Component> {
public:
BQ25186Button(uint8_t mask, uint8_t value) : mask_(mask), value_(value) {}
protected:
void press_action() override { this->parent_->write_ship_rst(this->mask_, this->value_); }
const uint8_t mask_;
const uint8_t value_;
};
} // namespace esphome::bq25186
#endif // USE_BUTTON
+4 -7
View File
@@ -1,4 +1,3 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
@@ -10,11 +9,10 @@ from esphome.const import (
)
from esphome.types import ConfigType
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID
DEPENDENCIES = ["bq25186"]
BQ25186Button = bq25186_ns.class_("BQ25186Button", button.Button)
# SHIP_RST: bit 7 is the software reset, bits 6:5 the shutdown, ship and hardware reset codes
SOFTWARE_RESET_MASK = 0x80
@@ -33,7 +31,7 @@ CONFIG_SCHEMA = cv.All(
BQ25186_CHILD_SCHEMA.extend(
{
cv.Optional(key): button.button_schema(
BQ25186Button,
button.ApplyButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=icon,
device_class=device_class,
@@ -46,7 +44,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_BQ25186_ID])
for key, mask, value, _, _ in _BUTTONS:
if (conf := config.get(key)) is not None:
var = await button.new_button(conf, mask, value)
await cg.register_parented(var, config[CONF_BQ25186_ID])
await buttons(key, None, apply=f"write_ship_rst({mask:#04x}, {value:#04x})")
@@ -15,27 +15,24 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component, dfrobot_sen0609_ns
DFRobotSEN0609Button = dfrobot_sen0609_ns.class_("DFRobotSEN0609Button", button.Button)
ButtonAction = dfrobot_sen0609_ns.enum("ButtonAction")
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
DFRobotSEN0609Button,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_QUERY_PARAMS): button.button_schema(
DFRobotSEN0609Button,
button.ApplyButton,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_DATABASE,
),
cv.Optional(CONF_RESTART): button.button_schema(
DFRobotSEN0609Button,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_RESTART,
@@ -47,11 +44,7 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(
CONF_FACTORY_RESET,
hub.set_factory_reset_button,
ButtonAction.BUTTON_FACTORY_RESET,
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
)
await buttons(
CONF_QUERY_PARAMS, hub.set_query_button, ButtonAction.BUTTON_QUERY_PARAMS
)
await buttons(CONF_RESTART, hub.set_restart_button, ButtonAction.BUTTON_RESTART)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="query_params()")
await buttons(CONF_RESTART, hub.set_restart_button, apply="restart()")
@@ -1,30 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../dfrobot_sen0609.h"
namespace esphome::dfrobot_sen0609 {
class DFRobotSEN0609Button final : public button::Button, public Parented<DFRobotSEN0609Component> {
public:
explicit DFRobotSEN0609Button(ButtonAction action) : action_(action) {}
protected:
void press_action() override {
switch (this->action_) {
case BUTTON_RESTART:
this->parent_->restart();
break;
case BUTTON_FACTORY_RESET:
this->parent_->factory_reset();
break;
case BUTTON_QUERY_PARAMS:
this->parent_->query_params();
break;
}
}
ButtonAction action_;
};
} // namespace esphome::dfrobot_sen0609
@@ -62,12 +62,6 @@ enum SwitchSetting : uint8_t {
SWITCH_COUNT,
};
enum ButtonAction : uint8_t {
BUTTON_RESTART,
BUTTON_FACTORY_RESET,
BUTTON_QUERY_PARAMS,
};
/// Module parameters; a write or read of each is one command. Pending work runs in this order.
enum Param : uint8_t {
PARAM_RUN_APP,
+2 -1
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import socket
from esphome.components import network, socket
from esphome.components.light.effects import register_addressable_effect
from esphome.components.light.types import AddressableLightEffect
import esphome.config_validation as cv
@@ -38,6 +38,7 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
network.require_ipv4("e131"),
socket.consume_sockets(1, "e131", socket.SocketType.UDP),
)
+10
View File
@@ -711,6 +711,15 @@ def set_core_data(config):
return config
def _require_ipv4_on_arduino(config: ConfigType) -> ConfigType:
"""The Arduino core's IPAddress.cpp does not compile with IPv4 disabled."""
if CORE.using_arduino:
from esphome.components import network
network.require_ipv4(PLATFORM_ESP32)(config)
return config
def get_esp32_variant(core_obj=None):
return (core_obj or CORE).data[KEY_ESP32][KEY_VARIANT]
@@ -2494,6 +2503,7 @@ CONFIG_SCHEMA = cv.All(
_set_default_framework,
_check_versions,
set_core_data,
_require_ipv4_on_arduino,
cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT),
)
+2
View File
@@ -7,6 +7,7 @@ from typing import Any
from esphome.build_helpers.native import native_backend
import esphome.codegen as cg
from esphome.components import network
import esphome.config_validation as cv
from esphome.const import (
CONF_BOARD,
@@ -369,6 +370,7 @@ CONFIG_SCHEMA = cv.All(
_warn_platformio_toolchain,
_validate_native_toolchain,
set_core_data,
network.require_ipv4(PLATFORM_ESP8266),
)
@@ -1,20 +1,18 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from .. import CONF_ESPECTRE_ID, ESPectreComponent, espectre_ns
from .. import CONF_ESPECTRE_ID, ESPectreComponent
DEPENDENCIES = ["espectre"]
RecalibrateButton = espectre_ns.class_("RecalibrateButton", button.Button)
CONFIG_SCHEMA = button.button_schema(
RecalibrateButton, entity_category=ENTITY_CATEGORY_CONFIG
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
).extend({cv.GenerateID(CONF_ESPECTRE_ID): cv.use_id(ESPectreComponent)})
async def to_code(config: ConfigType) -> None:
var = await button.new_button(config)
await cg.register_parented(var, config[CONF_ESPECTRE_ID])
buttons = button.sub_buttons(config, parent=config[CONF_ESPECTRE_ID])
await buttons.create(config, None, apply="recalibrate()")
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESPECTRE
#include "esphome/components/button/button.h"
#include "../espectre.h"
namespace esphome::espectre {
class RecalibrateButton final : public button::Button, public Parented<ESPectreComponent> {
protected:
void press_action() override { this->parent_->recalibrate(); }
};
} // namespace esphome::espectre
#endif // USE_ESPECTRE
+3
View File
@@ -8,6 +8,7 @@ from esphome.components.network import (
get_network_priority,
get_priority_interfaces_from_full_config,
ip_address_literal,
require_ipv4,
)
from esphome.config_helpers import (
filter_source_files_from_defines,
@@ -22,6 +23,7 @@ from esphome.const import (
CONF_DNS2,
CONF_DOMAIN,
CONF_ENABLE_ON_BOOT,
CONF_ETHERNET,
CONF_GATEWAY,
CONF_ID,
CONF_INTERRUPT_PIN,
@@ -556,6 +558,7 @@ CONFIG_SCHEMA = cv.All(
upper=True,
),
_validate,
require_ipv4(CONF_ETHERNET),
)
+8 -11
View File
@@ -1,12 +1,10 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.types import ConfigType
from ..climate import CONF_HAIER_ID, HonClimate, haier_ns
from ..climate import CONF_HAIER_ID, HonClimate
CODEOWNERS = ["@paveldn"]
SelfCleaningButton = haier_ns.class_("SelfCleaningButton", button.Button)
SteriCleaningButton = haier_ns.class_("SteriCleaningButton", button.Button)
# Haier buttons
@@ -20,19 +18,18 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.Optional(CONF_SELF_CLEANING): button.button_schema(
SelfCleaningButton,
button.ApplyButton,
icon=ICON_SPRAY_BOTTLE,
),
cv.Optional(CONF_STERI_CLEANING): button.button_schema(
SteriCleaningButton,
button.ApplyButton,
icon=ICON_SPRAY_BOTTLE,
),
}
)
async def to_code(config):
for button_type in [CONF_SELF_CLEANING, CONF_STERI_CLEANING]:
if conf := config.get(button_type):
btn = await button.new_button(conf)
await cg.register_parented(btn, config[CONF_HAIER_ID])
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_HAIER_ID])
await buttons(CONF_SELF_CLEANING, None, apply="start_self_cleaning()")
await buttons(CONF_STERI_CLEANING, None, apply="start_steri_cleaning()")
@@ -1,7 +0,0 @@
#include "self_cleaning.h"
namespace esphome::haier {
void SelfCleaningButton::press_action() { this->parent_->start_self_cleaning(); }
} // namespace esphome::haier
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hon_climate.h"
namespace esphome::haier {
class SelfCleaningButton final : public button::Button, public Parented<HonClimate> {
public:
SelfCleaningButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::haier
@@ -1,7 +0,0 @@
#include "steri_cleaning.h"
namespace esphome::haier {
void SteriCleaningButton::press_action() { this->parent_->start_steri_cleaning(); }
} // namespace esphome::haier
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hon_climate.h"
namespace esphome::haier {
class SteriCleaningButton final : public button::Button, public Parented<HonClimate> {
public:
SteriCleaningButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::haier
@@ -1,10 +1,9 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import ICON_AIR_FILTER
from esphome.types import ConfigType
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp, hoermann_hcp_ns
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp
DEPENDENCIES = ["hoermann_hcp"]
@@ -13,10 +12,6 @@ CONF_VENT = "vent"
ICON_GARAGE_OPEN_VARIANT = "mdi:garage-open-variant"
HoermannHcpVentButton = hoermann_hcp_ns.class_("HoermannHcpVentButton", button.Button)
HoermannHcpHalfOpenButton = hoermann_hcp_ns.class_(
"HoermannHcpHalfOpenButton", button.Button
)
BUTTON_KEYS = (CONF_VENT, CONF_HALF_OPEN)
@@ -25,10 +20,10 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(CONF_HOERMANN_HCP_ID): cv.use_id(HoermannHcp),
cv.Optional(CONF_VENT): button.button_schema(
HoermannHcpVentButton, icon=ICON_AIR_FILTER
button.ApplyButton, icon=ICON_AIR_FILTER
),
cv.Optional(CONF_HALF_OPEN): button.button_schema(
HoermannHcpHalfOpenButton, icon=ICON_GARAGE_OPEN_VARIANT
button.ApplyButton, icon=ICON_GARAGE_OPEN_VARIANT
),
}
),
@@ -37,7 +32,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_HOERMANN_HCP_ID])
for key in BUTTON_KEYS:
if (conf := config.get(key)) is not None:
await button.new_button(conf, parent)
buttons = button.sub_buttons(config, parent=config[CONF_HOERMANN_HCP_ID])
await buttons(CONF_VENT, None, apply="vent_door()")
await buttons(CONF_HALF_OPEN, None, apply="half_open_door()")
@@ -1,34 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hoermann_hcp.h"
namespace esphome::hoermann_hcp {
// The door commands the cover has no equivalent for. A refused command is already reported by the hub and
// leaves nothing to correct here, because a button carries no state of its own.
class HoermannHcpButton : public button::Button {
public:
explicit HoermannHcpButton(HoermannHcp *parent) : parent_(parent) {}
protected:
HoermannHcp *const parent_;
};
class HoermannHcpVentButton final : public HoermannHcpButton {
public:
using HoermannHcpButton::HoermannHcpButton;
protected:
void press_action() override { this->parent_->vent_door(); }
};
class HoermannHcpHalfOpenButton final : public HoermannHcpButton {
public:
using HoermannHcpButton::HoermannHcpButton;
protected:
void press_action() override { this->parent_->half_open_door(); }
};
} // namespace esphome::hoermann_hcp
+2
View File
@@ -1,6 +1,7 @@
import sys
import esphome.codegen as cg
from esphome.components import network
import esphome.config_validation as cv
from esphome.const import (
CONF_MAC_ADDRESS,
@@ -44,6 +45,7 @@ CONFIG_SCHEMA = cv.All(
# is no PlatformIO backend, so a CLI --toolchain must name this one
cv.resolve_toolchain("host", (Toolchain.HOST,), Toolchain.HOST),
set_core_data,
network.require_ipv4(PLATFORM_HOST),
)
@@ -107,13 +107,10 @@ def _set_stream_limits(config: ConfigType) -> ConfigType:
)(config)
return config
# The original ESP32 cannot lay out sub-16-bit slots that match ESPHome's packed audio, so the smallest
# stream it accepts is 16-bit (see start_i2s_driver); the other variants handle 8-bit.
min_bits_per_sample = 16 if esp32.get_esp32_variant() == esp32.VARIANT_ESP32 else 8
# The configured bits per sample sets the I2S slot width, but the speaker narrows wider streams down to it
# in place before clocking them out (see start_i2s_driver). Advertise up to 32-bit so those wider streams
# are accepted rather than forcing an upstream conversion.
# The configured bits per sample sets the I2S slot width, but the speaker converts streams of other widths
# before clocking them out (see start_i2s_driver), widening them on the original ESP32. Advertise 8- to 32-bit
# so those streams are accepted rather than forcing an upstream conversion.
min_bits_per_sample = 8
max_bits_per_sample = 32
if config[CONF_I2S_MODE] == CONF_PRIMARY:
@@ -153,10 +150,12 @@ def _validate_esp32_variant(config: ConfigType) -> ConfigType:
if variant not in INTERNAL_DAC_VARIANTS:
raise cv.Invalid(f"{variant} does not have an internal DAC")
elif variant == esp32.VARIANT_ESP32 and config[CONF_BITS_PER_SAMPLE] == 8:
# The original ESP32 I2S peripheral packs each sample into a whole number of 16-bit words, so an
# 8-bit slot does not line up with ESPHome's tightly packed audio (see start_i2s_driver). Reject it
# at config time rather than emitting corrupted output at runtime.
raise cv.Invalid("8-bit audio is not supported on the original ESP32")
# An 8-bit slot is rejected on the original ESP32: no known DAC needs one, and it is untested there.
# 8-bit streams still work; they are widened to the 16- or 32-bit slot (see start_i2s_driver).
raise cv.Invalid(
f"{CONF_BITS_PER_SAMPLE} cannot be 8bit on the original ESP32",
path=[CONF_BITS_PER_SAMPLE],
)
return config
@@ -208,8 +208,8 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
audio::AudioStreamInfo current_stream_info_; // Format of the audio in the ring buffer (the I2S input)
// Format actually clocked out of the I2S peripheral. Same channel count and sample rate as
// current_stream_info_, but the bits per sample may be narrower when the incoming stream is wider than
// the speaker's configured slot bit width. Set by start_i2s_driver before the speaker task starts.
// current_stream_info_, but the bits per sample may be converted to the speaker's configured slot bit width.
// Set by start_i2s_driver before the speaker task starts.
audio::AudioStreamInfo output_stream_info_;
gpio_num_t dout_pin_;
@@ -38,6 +38,11 @@ static constexpr size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT * 2;
// DMA_BUFFER_DURATION_MS, so a multiple of that gives plenty of slack against scheduling jitter
// without masking real failures.
static constexpr TickType_t WRITE_TIMEOUT_TICKS = pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * (DMA_BUFFERS_COUNT + 1));
#ifdef USE_ESP32_VARIANT_ESP32
// Stack buffer that widened audio passes through on the original ESP32. Small enough for the speaker task's stack,
// large enough that the extra i2s_channel_write calls cost little.
static constexpr size_t WIDEN_BUFFER_BYTES = 512;
#endif
// Requested frames per DMA buffer for the given stream, clamped so the byte size stays within the ESP-IDF
// maximum DMA descriptor size. This is only the value handed to the channel config: ESP-IDF may still adjust
@@ -66,9 +71,8 @@ void I2SAudioSpeaker::dump_config() {
}
ESP_LOGCONFIG(TAG, " Communication format: %s", fmt_str);
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
// The width of each I2S slot. It is also the narrowing ceiling: streams wider than this are narrowed to
// it. A stream narrower than the slot is left at its own width and clocked into the wider slot, so this
// is not necessarily the sample data width (which depends on the incoming stream).
// The width of each I2S slot. Wider streams are narrowed to it. Narrower streams are padded into the slot by
// the peripheral, except on the original ESP32, where they are widened to it.
ESP_LOGCONFIG(TAG, " Slot bit width: %u", (unsigned) static_cast<uint32_t>(this->slot_bit_width_));
}
}
@@ -92,11 +96,14 @@ void I2SAudioSpeaker::run_speaker_task() {
const size_t ring_buffer_size =
(this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / source_alignment) * source_alignment;
// Per-frame byte widths and whether the task must narrow the bit depth before writing to the I2S peripheral.
// Per-frame byte widths and whether the task must convert the bit depth before writing to the I2S peripheral.
const uint8_t channels = this->current_stream_info_.get_channels();
const uint8_t input_bytes_per_sample = this->current_stream_info_.get_bits_per_sample() / 8;
const uint8_t output_bytes_per_sample = this->output_stream_info_.get_bits_per_sample() / 8;
const bool narrowing = input_bytes_per_sample != output_bytes_per_sample;
const bool narrowing = input_bytes_per_sample > output_bytes_per_sample;
#ifdef USE_ESP32_VARIANT_ESP32
const bool widening = input_bytes_per_sample < output_bytes_per_sample;
#endif
// ESP-IDF may allocate smaller (or cache-line-rounded) DMA buffers than dma_buffer_frames() requested: it
// clamps each descriptor to the max DMA descriptor size and, on targets that route internal memory through
@@ -323,20 +330,26 @@ void I2SAudioSpeaker::run_speaker_task() {
const size_t output_bytes = this->output_stream_info_.frames_to_bytes(frames_to_write);
uint8_t *chunk = audio_source->mutable_data();
if (narrowing) {
// Narrow the bit depth in place: output exactly aliases input with the same channel count and a
// smaller width, which copy_frames handles as a single forward pass. Only the frames about to be
// consumed are overwritten, so any unprocessed tail stays intact for the next iteration.
esp_audio_libs::pcm_convert::copy_frames(chunk, chunk, input_bytes_per_sample, channels,
output_bytes_per_sample, channels, frames_to_write);
}
this->swap_esp32_mono_samples_(chunk, output_bytes);
size_t bw = 0;
i2s_channel_write(this->tx_handle_, chunk, output_bytes, &bw, WRITE_TIMEOUT_TICKS);
#ifdef USE_ESP32_VARIANT_ESP32
if (widening) {
bw = this->write_widened_(chunk, frames_to_write);
} else
#endif
{
if (narrowing) {
// Narrow the bit depth in place: output exactly aliases input with the same channel count and a
// smaller width, which copy_frames handles as a single forward pass. Only the frames about to be
// consumed are overwritten, so any unprocessed tail stays intact for the next iteration.
esp_audio_libs::pcm_convert::copy_frames(chunk, chunk, input_bytes_per_sample, channels,
output_bytes_per_sample, channels, frames_to_write);
}
this->swap_esp32_mono_samples_(chunk, output_bytes);
i2s_channel_write(this->tx_handle_, chunk, output_bytes, &bw, WRITE_TIMEOUT_TICKS);
}
if (bw != output_bytes) {
// A short write breaks DMA descriptor alignment for every subsequent event. Drop the chunk rather
// than retry it: it was already narrowed in place.
// than retry it: it may already have been converted in place.
ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw,
(unsigned) output_bytes);
audio_source->consume(input_bytes);
@@ -416,26 +429,26 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
return ESP_ERR_NOT_SUPPORTED;
}
// When the stream is wider than the configured slot bit width, the speaker task narrows each frame in place
// before handing it to the I2S peripheral. Compute the output format here so the driver, DMA buffers, and
// the task's conversion all agree on the clocked-out width. A stream no wider than the slot width is passed
// through unchanged (the slot may still be wider than the data, the existing behavior).
// Compute the output width so the driver, DMA buffers, and the task's conversion all agree. A stream wider than
// the configured slot is narrowed to it, and a narrower stream keeps its width for the peripheral to pad into the
// slot. The original ESP32 cannot clock a slot wider than its data, so there every stream is converted to the
// slot's memory width instead.
uint8_t output_bits_per_sample = audio_stream_info.get_bits_per_sample();
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
#ifdef USE_ESP32_VARIANT_ESP32
output_bits_per_sample = this->slot_memory_bits_();
#else
const uint8_t configured_bits = static_cast<uint8_t>(this->slot_bit_width_);
if (output_bits_per_sample > configured_bits) {
output_bits_per_sample = configured_bits;
}
#endif
}
this->output_stream_info_ = audio::AudioStreamInfo(output_bits_per_sample, audio_stream_info.get_channels(),
audio_stream_info.get_sample_rate());
#ifdef USE_ESP32_VARIANT_ESP32
// The original ESP32 I2S peripheral stores each sample in a whole number of 16-bit words (a 24-bit sample
// occupies 4 bytes in the DMA buffer, an 8-bit sample 2 bytes), but ESPHome's audio pipeline packs samples
// tightly (3 bytes for 24-bit, 1 for 8-bit). The two layouts only line up when the bit depth is a multiple
// of 16. The check is on the output width since that is what reaches the peripheral; a wider input is fine
// as long as it narrows to a 16- or 32-bit slot.
// Only a slot left at AUTO can reach this; every configured slot is converted to 16- or 32-bit samples above
if (output_bits_per_sample % 16 != 0) {
ESP_LOGE(TAG, "ESP32 supports only 16- or 32-bit output, got %u-bit", (unsigned) output_bits_per_sample);
return ESP_ERR_NOT_SUPPORTED;
@@ -487,8 +500,8 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
#ifdef USE_I2S_AUDIO_FULL_DUPLEX
audio::AudioStreamInfo I2SAudioSpeaker::full_duplex_stream_info_() const {
return audio::AudioStreamInfo(static_cast<uint8_t>(this->slot_bit_width_),
this->slot_mode_ == I2S_SLOT_MODE_STEREO ? 2 : 1, this->sample_rate_);
return audio::AudioStreamInfo(this->slot_memory_bits_(), this->slot_mode_ == I2S_SLOT_MODE_STEREO ? 2 : 1,
this->sample_rate_);
}
void I2SAudioSpeaker::build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) {
@@ -530,8 +543,14 @@ void I2SAudioSpeaker::build_i2s_config_(const audio::AudioStreamInfo &output_str
slot_mask = I2S_STD_SLOT_BOTH;
}
// Configure the data bit width from the output (post-narrowing) format, which is what is clocked out.
const i2s_data_bit_width_t data_bit_width = (i2s_data_bit_width_t) output_stream_info.get_bits_per_sample();
// Configure the data bit width from the output (post-conversion) format, which is what is clocked out.
i2s_data_bit_width_t data_bit_width = (i2s_data_bit_width_t) output_stream_info.get_bits_per_sample();
#ifdef USE_ESP32_VARIANT_ESP32
// The original ESP32 clocks data at the slot width; a 24-bit slot sends the top 24 bits of each 32-bit sample
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
data_bit_width = (i2s_data_bit_width_t) this->slot_bit_width_;
}
#endif
i2s_std_slot_config_t slot_cfg;
switch (this->i2s_comm_fmt_) {
case I2SCommFmt::PCM:
@@ -545,23 +564,10 @@ void I2SAudioSpeaker::build_i2s_config_(const audio::AudioStreamInfo &output_str
break;
}
#ifdef USE_ESP32_VARIANT_ESP32
// There seems to be a bug on the ESP32 (non-variant) platform where setting the slot bit width higher than the
// bits per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems
// to make it play at the correct speed while sending more bits per slot.
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
uint32_t configured_bit_width = static_cast<uint32_t>(this->slot_bit_width_);
slot_cfg.ws_width = configured_bit_width;
if (configured_bit_width > 16) {
slot_cfg.msb_right = false;
}
}
#else
slot_cfg.slot_bit_width = this->slot_bit_width_;
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
slot_cfg.ws_width = static_cast<uint32_t>(this->slot_bit_width_);
}
#endif // USE_ESP32_VARIANT_ESP32
slot_cfg.slot_mask = slot_mask;
i2s_std_gpio_config_t gpio_cfg = this->parent_->get_pin_config();
@@ -574,6 +580,45 @@ void I2SAudioSpeaker::build_i2s_config_(const audio::AudioStreamInfo &output_str
};
}
uint8_t I2SAudioSpeaker::slot_memory_bits_() const {
#ifdef USE_ESP32_VARIANT_ESP32
// The original ESP32 stores a 24-bit slot's samples in 32-bit words
if (this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_24BIT) {
return 32;
}
#endif
return static_cast<uint8_t>(this->slot_bit_width_);
}
#ifdef USE_ESP32_VARIANT_ESP32
size_t I2SAudioSpeaker::write_widened_(const uint8_t *input, uint32_t frames) {
alignas(4) uint8_t widened[WIDEN_BUFFER_BYTES];
const uint8_t channels = this->current_stream_info_.get_channels();
const uint8_t input_bytes_per_sample = this->current_stream_info_.get_bits_per_sample() / 8;
const uint8_t output_bytes_per_sample = this->output_stream_info_.get_bits_per_sample() / 8;
const uint32_t frames_per_pass = WIDEN_BUFFER_BYTES / this->output_stream_info_.frames_to_bytes(1);
size_t bytes_written = 0;
while (frames > 0) {
const uint32_t pass_frames = std::min(frames, frames_per_pass);
esp_audio_libs::pcm_convert::copy_frames(input, widened, input_bytes_per_sample, channels, output_bytes_per_sample,
channels, pass_frames);
const size_t pass_bytes = this->output_stream_info_.frames_to_bytes(pass_frames);
// 8-bit mono widened to 16-bit mono needs the pair swap; passes hold whole pairs since writes are pair-aligned
this->swap_esp32_mono_samples_(widened, pass_bytes);
size_t bw = 0;
i2s_channel_write(this->tx_handle_, widened, pass_bytes, &bw, WRITE_TIMEOUT_TICKS);
bytes_written += bw;
if (bw != pass_bytes) {
break;
}
input += this->current_stream_info_.frames_to_bytes(pass_frames);
frames -= pass_frames;
}
return bytes_written;
}
#endif // USE_ESP32_VARIANT_ESP32
} // namespace esphome::i2s_audio
#endif // USE_ESP32
@@ -38,6 +38,17 @@ class I2SAudioSpeaker final : public I2SAudioSpeakerBase {
audio::AudioStreamInfo full_duplex_stream_info_() const;
#endif
/// @brief Bits per sample that the configured slot takes in DMA memory.
uint8_t slot_memory_bits_() const;
#ifdef USE_ESP32_VARIANT_ESP32
/// @brief Widens frames to the output bit depth through a small stack buffer and writes them to the peripheral.
/// @param input Frames in the incoming stream format
/// @param frames Number of frames to widen and write
/// @return Bytes written, which is short of the widened size if a write timed out
size_t write_widened_(const uint8_t *input, uint32_t frames);
#endif
/// @brief Frame multiple for DMA buffers and writes; 2 keeps ESP32 mono sample pairs word-aligned.
static uint32_t dma_frame_multiple(const audio::AudioStreamInfo &stream_info) {
return esp32_mono_swap_needed(stream_info) ? 2 : 1;
+11 -11
View File
@@ -15,29 +15,25 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
FactoryResetButton = ld2410_ns.class_("FactoryResetButton", button.Button)
QueryButton = ld2410_ns.class_("QueryButton", button.Button)
RestartButton = ld2410_ns.class_("RestartButton", button.Button)
from .. import CONF_LD2410_ID, LD2410Component
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
FactoryResetButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_RESTART): button.button_schema(
RestartButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_RESTART,
),
cv.Optional(CONF_QUERY_PARAMS): button.button_schema(
QueryButton,
button.ApplyButton,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_DATABASE,
),
@@ -47,6 +43,10 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button)
await buttons(
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
)
await buttons(
CONF_RESTART, hub.set_restart_button, apply="restart_and_read_all_info()"
)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="read_all_info()")
@@ -1,7 +0,0 @@
#include "factory_reset_button.h"
namespace esphome::ld2410 {
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace esphome::ld2410
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class FactoryResetButton final : public button::Button, public Parented<LD2410Component> {
public:
FactoryResetButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2410
@@ -1,7 +0,0 @@
#include "query_button.h"
namespace esphome::ld2410 {
void QueryButton::press_action() { this->parent_->read_all_info(); }
} // namespace esphome::ld2410
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class QueryButton final : public button::Button, public Parented<LD2410Component> {
public:
QueryButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2410
@@ -1,7 +0,0 @@
#include "restart_button.h"
namespace esphome::ld2410 {
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
} // namespace esphome::ld2410
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class RestartButton final : public button::Button, public Parented<LD2410Component> {
public:
RestartButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2410
+13 -14
View File
@@ -16,38 +16,32 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component
from .. import CONF_LD2412_ID, LD2412Component
FactoryResetButton = LD2412_ns.class_("FactoryResetButton", button.Button)
QueryButton = LD2412_ns.class_("QueryButton", button.Button)
RestartButton = LD2412_ns.class_("RestartButton", button.Button)
StartDynamicBackgroundCorrectionButton = LD2412_ns.class_(
"StartDynamicBackgroundCorrectionButton", button.Button
)
CONF_START_DYNAMIC_BACKGROUND_CORRECTION = "start_dynamic_background_correction"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
FactoryResetButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_QUERY_PARAMS): button.button_schema(
QueryButton,
button.ApplyButton,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_DATABASE,
),
cv.Optional(CONF_RESTART): button.button_schema(
RestartButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_RESTART,
),
cv.Optional(CONF_START_DYNAMIC_BACKGROUND_CORRECTION): button.button_schema(
StartDynamicBackgroundCorrectionButton,
button.ApplyButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_PULSE,
),
@@ -57,10 +51,15 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2412_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="read_all_info()")
await buttons(
CONF_RESTART, hub.set_restart_button, apply="restart_and_read_all_info()"
)
await buttons(
CONF_START_DYNAMIC_BACKGROUND_CORRECTION,
hub.set_start_dynamic_background_correction_button,
apply="start_dynamic_background_correction()",
)
@@ -1,7 +0,0 @@
#include "factory_reset_button.h"
namespace esphome::ld2412 {
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace esphome::ld2412
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class FactoryResetButton final : public button::Button, public Parented<LD2412Component> {
public:
FactoryResetButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2412
@@ -1,7 +0,0 @@
#include "query_button.h"
namespace esphome::ld2412 {
void QueryButton::press_action() { this->parent_->read_all_info(); }
} // namespace esphome::ld2412
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class QueryButton final : public button::Button, public Parented<LD2412Component> {
public:
QueryButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2412
@@ -1,7 +0,0 @@
#include "restart_button.h"
namespace esphome::ld2412 {
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
} // namespace esphome::ld2412
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class RestartButton final : public button::Button, public Parented<LD2412Component> {
public:
RestartButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2412
@@ -1,9 +0,0 @@
#include "start_dynamic_background_correction_button.h"
#include "restart_button.h"
namespace esphome::ld2412 {
void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); }
} // namespace esphome::ld2412
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class StartDynamicBackgroundCorrectionButton final : public button::Button, public Parented<LD2412Component> {
public:
StartDynamicBackgroundCorrectionButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2412
+23 -14
View File
@@ -12,12 +12,7 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns
LD2420ApplyConfigButton = ld2420_ns.class_("LD2420ApplyConfigButton", button.Button)
LD2420RevertConfigButton = ld2420_ns.class_("LD2420RevertConfigButton", button.Button)
LD2420RestartModuleButton = ld2420_ns.class_("LD2420RestartModuleButton", button.Button)
LD2420FactoryResetButton = ld2420_ns.class_("LD2420FactoryResetButton", button.Button)
from .. import CONF_LD2420_ID, LD2420Component
CONF_APPLY_CONFIG = "apply_config"
CONF_REVERT_CONFIG = "revert_config"
@@ -27,24 +22,24 @@ CONF_RESTART_MODULE = "restart_module"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component),
cv.Required(CONF_APPLY_CONFIG): button.button_schema(
LD2420ApplyConfigButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_REVERT_CONFIG): button.button_schema(
LD2420RevertConfigButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART,
),
cv.Optional(CONF_RESTART_MODULE): button.button_schema(
LD2420RestartModuleButton,
button.ApplyButton,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_DATABASE,
),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
LD2420FactoryResetButton,
button.ApplyButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_DATABASE,
),
@@ -54,7 +49,21 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2420_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_APPLY_CONFIG, hub.set_apply_config_button)
await buttons(CONF_REVERT_CONFIG, hub.set_revert_config_button)
await buttons(CONF_RESTART_MODULE, hub.set_restart_module_button)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(
CONF_APPLY_CONFIG, hub.set_apply_config_button, apply="apply_config_action()"
)
await buttons(
CONF_REVERT_CONFIG,
hub.set_revert_config_button,
apply="revert_config_action()",
)
await buttons(
CONF_RESTART_MODULE,
hub.set_restart_module_button,
apply="restart_module_action()",
)
await buttons(
CONF_FACTORY_RESET,
hub.set_factory_reset_button,
apply="factory_reset_action()",
)
@@ -1,14 +0,0 @@
#include "reconfig_buttons.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
ESPHOME_LOG_TAG(TAG, "ld2420.button");
namespace esphome::ld2420 {
void LD2420ApplyConfigButton::press_action() { this->parent_->apply_config_action(); }
void LD2420RevertConfigButton::press_action() { this->parent_->revert_config_action(); }
void LD2420RestartModuleButton::press_action() { this->parent_->restart_module_action(); }
void LD2420FactoryResetButton::press_action() { this->parent_->factory_reset_action(); }
} // namespace esphome::ld2420
@@ -1,40 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2420.h"
namespace esphome::ld2420 {
class LD2420ApplyConfigButton final : public button::Button, public Parented<LD2420Component> {
public:
LD2420ApplyConfigButton() = default;
protected:
void press_action() override;
};
class LD2420RevertConfigButton final : public button::Button, public Parented<LD2420Component> {
public:
LD2420RevertConfigButton() = default;
protected:
void press_action() override;
};
class LD2420RestartModuleButton final : public button::Button, public Parented<LD2420Component> {
public:
LD2420RestartModuleButton() = default;
protected:
void press_action() override;
};
class LD2420FactoryResetButton final : public button::Button, public Parented<LD2420Component> {
public:
LD2420FactoryResetButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::ld2420
+9 -8
View File
@@ -13,22 +13,19 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
FactoryResetButton = ld2450_ns.class_("FactoryResetButton", button.Button)
RestartButton = ld2450_ns.class_("RestartButton", button.Button)
from .. import CONF_LD2450_ID, LD2450Component
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
FactoryResetButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_RESTART): button.button_schema(
RestartButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_RESTART,
@@ -39,5 +36,9 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
)
await buttons(
CONF_RESTART, hub.set_restart_button, apply="restart_and_read_all_info()"
)
@@ -1,7 +0,0 @@
#include "factory_reset_button.h"
namespace esphome::ld2450 {
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace esphome::ld2450
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2450.h"
namespace esphome::ld2450 {
class FactoryResetButton : public button::Button, public Parented<LD2450Component> {
public:
// User provided, not "= default": `new(p) FactoryResetButton()` would zero-fill .bss that is already zero.
FactoryResetButton() {}
protected:
void press_action() override;
};
} // namespace esphome::ld2450
@@ -1,7 +0,0 @@
#include "restart_button.h"
namespace esphome::ld2450 {
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
} // namespace esphome::ld2450
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2450.h"
namespace esphome::ld2450 {
class RestartButton : public button::Button, public Parented<LD2450Component> {
public:
// User provided, not "= default": `new(p) RestartButton()` would zero-fill .bss that is already zero.
RestartButton() {}
protected:
void press_action() override;
};
} // namespace esphome::ld2450
+11
View File
@@ -3,6 +3,7 @@ import logging
from pathlib import Path
import esphome.codegen as cg
from esphome.components.const import CONF_LIBRETINY
import esphome.config_validation as cv
from esphome.const import (
CONF_BOARD,
@@ -304,6 +305,15 @@ FRAMEWORK_SCHEMA = cv.All(
CONFIG_SCHEMA = cv.All(_notify_old_style, cv.require_platformio_toolchain("LibreTiny"))
def _require_ipv4(config: dict) -> dict:
"""Declares that LibreTiny builds need IPv4."""
# Imported here: logger imports this module, so a module level import would load network for every config
from esphome.components import network
return network.require_ipv4(CONF_LIBRETINY)(config)
BASE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(LTComponent),
@@ -318,6 +328,7 @@ BASE_SCHEMA = cv.Schema(
BASE_SCHEMA.add_extra(_detect_variant)
BASE_SCHEMA.add_extra(cv.require_platformio_toolchain("LibreTiny"))
BASE_SCHEMA.add_extra(_update_core_data)
BASE_SCHEMA.add_extra(_require_ipv4)
def _configure_lwip(config: dict) -> None:
+3 -1
View File
@@ -1,6 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import logger, socket
from esphome.components import logger, network, socket
from esphome.components.esp32 import (
add_idf_component,
idf_version,
@@ -35,6 +35,7 @@ from esphome.const import (
CONF_KEEPALIVE,
CONF_LEVEL,
CONF_LOG_TOPIC,
CONF_MQTT,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_ON_JSON_MESSAGE,
@@ -365,6 +366,7 @@ CONFIG_SCHEMA = cv.All(
]
),
_consume_mqtt_sockets,
network.require_ipv4(CONF_MQTT),
)
+122 -50
View File
@@ -1,3 +1,5 @@
from collections.abc import Callable
from dataclasses import dataclass, field
import ipaddress
import logging
from typing import Any
@@ -51,6 +53,16 @@ TCPIP_RECVMBOX_OPTIMIZED = 64
TCP_SEND_BUFFER_MIN = 2880
TCP_SEND_BUFFER_MAX = 65535
CONF_ENABLE_IPV4 = "enable_ipv4"
@dataclass
class NetworkData:
require_ipv4: set[str] = field(default_factory=set)
require_ipv6: set[str] = field(default_factory=set)
request_ipv4_off: set[str] = field(default_factory=set)
# Network priority tracking infrastructure
# Components can query this to determine their relative setup priority.
# CORE.data[KEY_NETWORK_PRIORITY] is a list of dicts of the form
@@ -264,11 +276,35 @@ def has_high_performance_networking() -> bool:
return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False)
def validate_ipv6(value: bool) -> bool:
if CORE.is_nrf52 and not value:
raise cv.Invalid("On nRF52, enable_ipv6 must be true")
def _get_data() -> NetworkData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = NetworkData()
return CORE.data[DOMAIN]
return value
def _ip_request(
update: Callable[[NetworkData], None],
) -> Callable[[ConfigType], ConfigType]:
def validator(config: ConfigType) -> ConfigType:
update(_get_data())
return config
return validator
def require_ipv4(name: str) -> Callable[[ConfigType], ConfigType]:
"""Return a validator declaring that component `name` needs IPv4."""
return _ip_request(lambda data: data.require_ipv4.add(name))
def require_ipv6(name: str) -> Callable[[ConfigType], ConfigType]:
"""Return a validator declaring that component `name` needs IPv6."""
return _ip_request(lambda data: data.require_ipv6.add(name))
def request_ipv4_off(name: str) -> Callable[[ConfigType], ConfigType]:
"""Return a validator asking for IPv4 to default to off, on behalf of `name`."""
return _ip_request(lambda data: data.request_ipv4_off.add(name))
def get_network_priority(iface: str) -> float | None:
@@ -348,33 +384,30 @@ def _validate_priority_list(value: Any) -> list[dict[str, str]]:
return entries
# Shared so _final_validate()'s auto-flip to True enforces the same version floor as
# the schema does for a user-typed 'enable_ipv6: true'.
_ENABLE_IPV6_FRAMEWORK_VERSION_VALIDATOR = cv.require_framework_version(
bk72xx_arduino=cv.Version(1, 7, 0),
esp_idf=cv.Version(0, 0, 0),
esp32_arduino=cv.Version(0, 0, 0),
esp8266_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0),
rp2_arduino=cv.Version(0, 0, 0),
nrf52_zephyr=cv.Version(0, 0, 0),
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(NetworkComponent),
cv.SplitDefault(
CONF_ENABLE_IPV6,
bk72xx=False,
esp32=False,
esp8266=False,
host=False,
rp2=False,
nrf52=True,
): cv.All(
cv.Optional(CONF_ENABLE_IPV4): cv.boolean,
cv.Optional(CONF_ENABLE_IPV6): cv.All(
cv.boolean,
cv.Any(
cv.require_framework_version(
bk72xx_arduino=cv.Version(1, 7, 0),
esp_idf=cv.Version(0, 0, 0),
esp32_arduino=cv.Version(0, 0, 0),
esp8266_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0),
rp2_arduino=cv.Version(0, 0, 0),
nrf52_zephyr=cv.Version(0, 0, 0),
),
_ENABLE_IPV6_FRAMEWORK_VERSION_VALIDATOR,
cv.boolean_false,
),
validate_ipv6,
),
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(
@@ -392,13 +425,50 @@ CONFIG_SCHEMA = cv.All(
)
def _resolve_ip_family(
config: ConfigType, key: str, family: str, requirers: set[str], default: bool
) -> bool:
"""Resolve `key` to a bool; return True when a component requires the family."""
if not requirers:
config.setdefault(key, default)
return False
if config.get(key) is False:
raise cv.Invalid(
f"{family} is required by {', '.join(sorted(requirers))}, but "
f"'network: {key}: false' explicitly disables it"
)
config[key] = True
return True
def _final_validate(config: ConfigType) -> None:
"""Check that every interface named in 'priority' has a corresponding component block."""
full = fv.full_config.get()
full_config = fv.full_config.get()
data = _get_data()
if _resolve_ip_family(
config, CONF_ENABLE_IPV6, "IPv6", data.require_ipv6, default=False
):
# Turning IPv6 on must not bypass the version gate cv.Any() enforces.
_ENABLE_IPV6_FRAMEWORK_VERSION_VALIDATOR(True)
_resolve_ip_family(
config,
CONF_ENABLE_IPV4,
"IPv4",
data.require_ipv4,
default=not data.request_ipv4_off,
)
if not config[CONF_ENABLE_IPV4] and not config[CONF_ENABLE_IPV6]:
raise cv.Invalid(
"'enable_ipv4: false' requires 'enable_ipv6: true' -- disabling both "
"leaves no IP stack"
)
# Check that every interface named in 'priority' has a corresponding component block.
priority_list = config.get(CONF_PRIORITY, [])
for entry in priority_list:
iface = entry["interface"]
if iface not in full:
if iface not in full_config:
raise cv.Invalid(
f"'{iface}' is listed in 'network: priority:' but no '{iface}:' "
f"component is configured",
@@ -430,7 +500,8 @@ FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.NETWORK)
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_NETWORK")
# ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed
enable_ipv6 = config[CONF_ENABLE_IPV6]
enable_ipv4 = config[CONF_ENABLE_IPV4]
# Store the user-declared network priority list in CORE.data so that ethernet,
# wifi and other network components can query it via get_network_priority()
@@ -557,7 +628,8 @@ async def to_code(config: ConfigType) -> None:
if CORE.is_nrf52:
zephyr_add_prj_conf("NETWORKING", True)
zephyr_add_prj_conf("NET_IPV6", True)
zephyr_add_prj_conf("NET_IPV4", enable_ipv4)
zephyr_add_prj_conf("NET_IPV6", enable_ipv6)
zephyr_add_prj_conf("NET_TCP", True)
zephyr_add_prj_conf("NET_UDP", True)
# The nRF Connect SDK replaces mbedTLS with PSA/Oberon crypto and does not provide the
@@ -582,28 +654,28 @@ async def to_code(config: ConfigType) -> None:
zephyr_add_prj_conf("NET_TCP_MAX_RECV_WINDOW_SIZE", 2280)
zephyr_add_prj_conf("NET_TCP_MAX_SEND_WINDOW_SIZE", 2280)
if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None:
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
if enable_ipv6:
cg.add_define(
"USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT]
)
if CORE.is_esp32:
if CORE.using_arduino:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", True)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", True)
else:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6)
elif enable_ipv6:
cg.add_build_flag("-DCONFIG_LWIP_IPV6")
cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG")
if CORE.is_bk72xx:
cg.add_build_flag("-DCONFIG_IPV6")
if CORE.is_esp8266:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY")
if CORE.is_rp2:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6")
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
if enable_ipv6:
cg.add_define(
"USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT]
)
if CORE.is_esp32:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6)
elif enable_ipv6:
cg.add_build_flag("-DCONFIG_LWIP_IPV6")
cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG")
if CORE.is_bk72xx:
cg.add_build_flag("-DCONFIG_IPV6")
if CORE.is_esp8266:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY")
if CORE.is_rp2:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6")
cg.add_define("USE_NETWORK_IPV4", enable_ipv4)
if CORE.is_esp32:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV4", enable_ipv4)
# Pvariable creation lives in a separate coroutine at NETWORK_SERVICES so it
# emits after wifi/ethernet at COMMUNICATION. This keeps compile-time config
# (above) separate from C++ object lifecycle and allows wiring in interface
+106 -29
View File
@@ -15,14 +15,14 @@
#if USE_ARDUINO
#include <Arduino.h>
#include <IPAddress.h>
#endif /* USE_ADRDUINO */
#endif /* USE_ARDUINO */
#ifdef USE_HOST
#include <arpa/inet.h>
#if USE_NETWORK_IPV6
using ip4_addr_t = struct in_addr;
using ip6_addr_t = struct in6_addr;
struct ip_addr_t {
struct ip_addr_t { // NOLINT(readability-identifier-naming) mirrors the lwIP name the other platforms use
union {
struct in6_addr ip6;
struct in_addr ip4;
@@ -55,9 +55,23 @@ using ip4_addr_t = in_addr;
#include <zephyr/net/net_ip.h>
#include <zephyr/net/socket.h>
#include <zephyr/posix/arpa/inet.h>
using ip_addr_t = struct in6_addr;
static inline int ipaddr_aton(const char *cp, ip_addr_t *addr) { return inet_pton(AF_INET6, cp, addr) == 1 ? 1 : 0; }
#ifndef CONFIG_NET_IPV6
#error "Zephyr builds need CONFIG_NET_IPV6"
#endif
using ip_addr_t = struct in6_addr;
static inline int ipaddr_aton(const char *cp, ip_addr_t *addr) {
#if defined(CONFIG_NET_IPV4)
// Dual-stack: a plain IPv4 literal must still parse -- store it as a v4-mapped
// IPv6 address (::ffff:a.b.c.d), matching the 4-octet constructor below.
struct in_addr addr4;
if (inet_pton(AF_INET, cp, &addr4) == 1) {
net_ipv6_addr_create_v4_mapped(&addr4, addr);
return 1;
}
#endif /* CONFIG_NET_IPV4 */
return inet_pton(AF_INET6, cp, addr) == 1 ? 1 : 0;
}
#endif /* USE_ZEPHYR */
#if USE_ESP32_FRAMEWORK_ARDUINO
#define arduino_ns Arduino_h
@@ -65,16 +79,24 @@ static inline int ipaddr_aton(const char *cp, ip_addr_t *addr) { return inet_pto
#define arduino_ns arduino
#elif USE_ARDUINO
#define arduino_ns
#endif
#endif /* USE_ESP32_FRAMEWORK_ARDUINO */
#ifdef USE_ESP32
#include <esp_netif.h>
#endif
#endif /* USE_ESP32 */
namespace esphome::network {
/// Buffer size for IP address string (IPv6 max: 39 chars + null)
static constexpr size_t IP_ADDRESS_BUFFER_SIZE = 40;
static constexpr size_t IP_ADDRESS_BUFFER_SIZE =
#if defined(USE_ZEPHYR)
// Mainline Zephyr's inet_ntop() (net_addr_ntop() in subsys/net/ip/utils.c) never checks
// its size argument, so an undersized buffer overflows instead of failing cleanly. Size
// for the worst case: an IPv4-mapped IPv6 address needs up to INET6_ADDRSTRLEN (46).
46;
#else
40;
#endif
/// Lowercase hex digits in IP address string (A-F -> a-f for IPv6 per RFC 5952)
inline void lowercase_ip_str(char *buf) {
@@ -86,19 +108,58 @@ inline void lowercase_ip_str(char *buf) {
struct IPAddress {
public:
#ifdef USE_ZEPHYR
#if defined(USE_ZEPHYR)
// With CONFIG_NET_IPV4 (dual stack), IPv4 addresses are stored v4-mapped (::ffff:a.b.c.d).
IPAddress() { memset(&ip_addr_, 0, sizeof(ip_addr_)); }
#if defined(CONFIG_NET_IPV4)
IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) {
struct in_addr addr4{
.s_addr = htonl(((uint32_t) first << 24) | ((uint32_t) second << 16) | ((uint32_t) third << 8) | fourth)};
net_ipv6_addr_create_v4_mapped(&addr4, &ip_addr_);
}
#endif /* CONFIG_NET_IPV4 */
IPAddress(const std::string &in_address) : ip_addr_{} { ipaddr_aton(in_address.c_str(), &ip_addr_); }
IPAddress(const struct in6_addr *other_ip) { ip_addr_ = *other_ip; }
IPAddress(const struct sockaddr_in6 *addr) { ip_addr_ = addr->sin6_addr; }
operator struct in6_addr() const { return ip_addr_; }
bool is_set() const { return !net_ipv6_is_addr_unspecified(&ip_addr_); }
bool is_set() const {
#if defined(CONFIG_NET_IPV4)
if (net_ipv6_addr_is_v4_mapped(&ip_addr_))
return ip_addr_.s6_addr32[3] != 0;
#endif /* CONFIG_NET_IPV4 */
return !net_ipv6_is_addr_unspecified(&ip_addr_);
}
#if defined(CONFIG_NET_IPV4)
// V4-mapped (::ffff:a.b.c.d) addresses represent IPv4 traffic carried over the AF_INET6 socket
// Zephyr always uses when CONFIG_NET_IPV6 is on; anything else is real IPv6.
bool is_ip4() const { return this->is_set() && net_ipv6_addr_is_v4_mapped(&ip_addr_); }
bool is_ip6() const { return this->is_set() && !net_ipv6_addr_is_v4_mapped(&ip_addr_); }
#else
bool is_ip4() const { return false; }
bool is_ip6() const { return this->is_set(); }
bool is_multicast() const { return net_ipv6_is_addr_mcast(&ip_addr_); }
#endif /* CONFIG_NET_IPV4 */
bool is_multicast() const {
#if defined(CONFIG_NET_IPV4)
if (net_ipv6_addr_is_v4_mapped(&ip_addr_)) {
struct in_addr addr4{};
addr4.s_addr = ip_addr_.s6_addr32[3];
return net_ipv4_is_addr_mcast(&addr4);
}
#endif /* CONFIG_NET_IPV4 */
return net_ipv6_is_addr_mcast(&ip_addr_);
}
char *str_to(char *buf) const {
#if defined(CONFIG_NET_IPV4)
if (this->is_ip4()) {
struct in_addr addr4{};
addr4.s_addr = ip_addr_.s6_addr32[3];
if (inet_ntop(AF_INET, &addr4, buf, IP_ADDRESS_BUFFER_SIZE) == nullptr)
buf[0] = '\0';
return buf;
}
#endif /* CONFIG_NET_IPV4 */
if (inet_ntop(AF_INET6, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE) == nullptr)
buf[0] = '\0';
return buf;
@@ -134,8 +195,8 @@ struct IPAddress {
operator ip_addr_t() const { return this->ip_addr_; }
bool is_set() const {
if (this->ip_addr_.type == IPADDR_TYPE_V6) {
static constexpr uint8_t zero[sizeof(struct in6_addr)] = {};
return memcmp(this->ip_addr_.u_addr.ip6.s6_addr, zero, sizeof(zero)) != 0;
static constexpr uint8_t ZERO[sizeof(struct in6_addr)] = {};
return memcmp(this->ip_addr_.u_addr.ip6.s6_addr, ZERO, sizeof(ZERO)) != 0;
}
return this->ip_addr_.u_addr.ip4.s_addr != 0;
}
@@ -187,59 +248,72 @@ struct IPAddress {
return buf; // IPv4 only, no hex letters to lowercase
}
#endif // USE_NETWORK_IPV6
#else
#else // LWIP
IPAddress() { ip_addr_set_zero(&ip_addr_); }
#if LWIP_IPV4
IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) {
IP_ADDR4(&ip_addr_, first, second, third, fourth);
}
#endif /* LWIP_IPV4 */
IPAddress(const ip_addr_t *other_ip) { ip_addr_copy(ip_addr_, *other_ip); }
IPAddress(const char *in_address) { ipaddr_aton(in_address, &ip_addr_); }
IPAddress(const std::string &in_address) { ipaddr_aton(in_address.c_str(), &ip_addr_); }
#if LWIP_IPV4
IPAddress(ip4_addr_t *other_ip) {
memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip4_addr_t));
#if LWIP_IPV6
ip_addr_.type = IPADDR_TYPE_V4;
#endif
#endif /* LWIP_IPV6 */
}
#endif /* LWIP_IPV4 */
#if USE_ARDUINO
IPAddress(const arduino_ns::IPAddress &other_ip) { ip_addr_set_ip4_u32(&ip_addr_, other_ip); }
#endif
#endif /* USE_ARDUINO */
#if LWIP_IPV6
IPAddress(ip6_addr_t *other_ip) {
memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip6_addr_t));
ip_addr_.type = IPADDR_TYPE_V6;
IP_SET_TYPE_VAL(ip_addr_, IPADDR_TYPE_V6);
}
#endif /* LWIP_IPV6 */
#ifdef USE_ESP32
#if LWIP_IPV6
IPAddress(esp_ip6_addr_t *other_ip) {
memcpy((void *) &ip_addr_.u_addr.ip6, (void *) other_ip, sizeof(esp_ip6_addr_t));
ip_addr_.type = IPADDR_TYPE_V6;
memcpy((void *) ip_2_ip6(&ip_addr_), (void *) other_ip, sizeof(esp_ip6_addr_t));
IP_SET_TYPE_VAL(ip_addr_, IPADDR_TYPE_V6);
}
#endif /* LWIP_IPV6 */
#if LWIP_IPV4
IPAddress(esp_ip4_addr_t *other_ip) {
memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t));
#if LWIP_IPV6
ip_addr_.type = IPADDR_TYPE_V4;
#endif
#endif /* LWIP_IPV6 */
}
#endif /* LWIP_IPV4 */
IPAddress(esp_ip_addr_t *other_ip) {
#if LWIP_IPV6
memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip_addr_));
#else
#elif LWIP_IPV4
memcpy((void *) &ip_addr_, (void *) &other_ip->u_addr.ip4, sizeof(ip_addr_));
#endif
#endif /* LWIP_IPV6 */
}
operator esp_ip_addr_t() const {
esp_ip_addr_t tmp;
esp_ip_addr_t tmp{};
#if LWIP_IPV6
memcpy((void *) &tmp, (void *) &ip_addr_, sizeof(ip_addr_));
#else
#if !LWIP_IPV4
tmp.type = IPADDR_TYPE_V6;
#endif /* !LWIP_IPV4 */
#elif LWIP_IPV4
memcpy((void *) &tmp.u_addr.ip4, (void *) &ip_addr_, sizeof(ip_addr_));
tmp.type = IPADDR_TYPE_V4;
#else
#error "Neither LWIP_IPV4 nor LWIP_IPV6 is enabled"
#endif /* LWIP_IPV6 */
return tmp;
}
#if LWIP_IPV4
operator esp_ip4_addr_t() const {
esp_ip4_addr_t tmp;
#if LWIP_IPV6
@@ -249,16 +323,17 @@ struct IPAddress {
#endif /* LWIP_IPV6 */
return tmp;
}
#endif /* LWIP_IPV4 */
#endif /* USE_ESP32 */
operator ip_addr_t() const { return ip_addr_; }
#if LWIP_IPV6
#if LWIP_IPV6 && LWIP_IPV4
operator ip4_addr_t() const { return *ip_2_ip4(&ip_addr_); }
#endif /* LWIP_IPV6 */
#endif /* LWIP_IPV6 && LWIP_IPV4 */
#if USE_ARDUINO
operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); }
#endif
#endif /* USE_ARDUINO */
bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
@@ -273,17 +348,19 @@ struct IPAddress {
}
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
#if LWIP_IPV4
IPAddress &operator+=(uint8_t increase) {
if (IP_IS_V4(&ip_addr_)) {
#if LWIP_IPV6
(((u8_t *) (&ip_addr_.u_addr.ip4))[3]) += increase;
#else
(((u8_t *) (&ip_addr_.addr))[3]) += increase;
#endif /* LWIP_IPV6 */
#endif /* LWIP_IPV6 */
}
return *this;
}
#endif
#endif /* LWIP_IPV4 */
#endif // LWIP
protected:
ip_addr_t ip_addr_;
@@ -292,4 +369,4 @@ struct IPAddress {
using IPAddresses = std::array<IPAddress, 5>;
} // namespace esphome::network
#endif
#endif /* NETWORK */
+3
View File
@@ -11,6 +11,7 @@ from esphome import pins
from esphome.build_helpers import pch
from esphome.build_helpers.ccache import resolve_ccache_path
import esphome.codegen as cg
from esphome.components import network
from esphome.components.zephyr import (
Section,
add_extra_script,
@@ -322,6 +323,8 @@ CONFIG_SCHEMA = cv.All(
_default_self_reset_pin,
_resolve_toolchain,
set_framework,
network.require_ipv6(PLATFORM_NRF52),
network.request_ipv4_off(PLATFORM_NRF52),
)
+4 -8
View File
@@ -31,7 +31,7 @@ from esphome.components.esp32 import (
require_vfs_select,
)
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
from esphome.components.network import DOMAIN as NETWORK_DOMAIN, add_use_address
from esphome.components.network import add_use_address, request_ipv4_off, require_ipv6
from esphome.components.nrf52.framework import include_west_project
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
@@ -40,7 +40,6 @@ from esphome.const import (
CONF_AP,
CONF_BAUD_RATE,
CONF_CHANNEL,
CONF_ENABLE_IPV6,
CONF_ENABLE_ON_BOOT,
CONF_FRAMEWORK,
CONF_HARDWARE_UART,
@@ -50,6 +49,7 @@ from esphome.const import (
CONF_LOGGER,
CONF_NETWORKS,
CONF_NUMBER,
CONF_OPENTHREAD,
CONF_OUTPUT_POWER,
CONF_RESET_PIN,
CONF_RX_PIN,
@@ -448,17 +448,13 @@ CONFIG_SCHEMA = cv.All(
_validate_platform,
_validate,
_require_vfs,
require_ipv6(CONF_OPENTHREAD),
request_ipv4_off(CONF_OPENTHREAD),
)
def _final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
network_config = full_config.get(NETWORK_DOMAIN, {})
if not network_config.get(CONF_ENABLE_IPV6, False):
raise cv.Invalid(
"OpenThread requires IPv6 to be enabled in the network component. "
"Please set `enable_ipv6: true` in the `network` configuration."
)
wifi_config = full_config.get(CONF_WIFI)
if CONF_BORDER_ROUTER not in config:
+2
View File
@@ -7,6 +7,7 @@ import subprocess
from typing import Any
import esphome.codegen as cg
from esphome.components import network
import esphome.config_validation as cv
from esphome.const import (
CONF_BOARD,
@@ -318,6 +319,7 @@ CONFIG_SCHEMA = cv.All(
_detect_variant,
cv.require_platformio_toolchain("RP2"),
set_core_data,
network.require_ipv4(PLATFORM_RP2),
)
@@ -9,23 +9,20 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_MR24HPC1_ID, MR24HPC1Component, mr24hpc1_ns
RestartButton = mr24hpc1_ns.class_("RestartButton", button.Button)
CustomSetEndButton = mr24hpc1_ns.class_("CustomSetEndButton", button.Button)
from .. import CONF_MR24HPC1_ID, MR24HPC1Component
CONF_CUSTOM_SET_END = "custom_set_end"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_MR24HPC1_ID): cv.use_id(MR24HPC1Component),
cv.Optional(CONF_RESTART): button.button_schema(
RestartButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_CUSTOM_SET_END): button.button_schema(
CustomSetEndButton,
button.ApplyButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:cog",
),
@@ -35,5 +32,9 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_MR24HPC1_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(CONF_CUSTOM_SET_END, hub.set_custom_set_end_button)
await buttons(CONF_RESTART, hub.set_restart_button, apply="set_restart()")
await buttons(
CONF_CUSTOM_SET_END,
hub.set_custom_set_end_button,
apply="set_custom_end_mode()",
)
@@ -1,7 +0,0 @@
#include "custom_mode_end_button.h"
namespace esphome::seeed_mr24hpc1 {
void CustomSetEndButton::press_action() { this->parent_->set_custom_end_mode(); }
} // namespace esphome::seeed_mr24hpc1
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../seeed_mr24hpc1.h"
namespace esphome::seeed_mr24hpc1 {
class CustomSetEndButton final : public button::Button, public Parented<MR24HPC1Component> {
public:
CustomSetEndButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::seeed_mr24hpc1
@@ -1,7 +0,0 @@
#include "restart_button.h"
namespace esphome::seeed_mr24hpc1 {
void RestartButton::press_action() { this->parent_->set_restart(); }
} // namespace esphome::seeed_mr24hpc1
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../seeed_mr24hpc1.h"
namespace esphome::seeed_mr24hpc1 {
class RestartButton final : public button::Button, public Parented<MR24HPC1Component> {
public:
RestartButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::seeed_mr24hpc1
@@ -10,24 +10,22 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_MR60FDA2_ID, MR60FDA2Component, mr60fda2_ns
from .. import CONF_MR60FDA2_ID, MR60FDA2Component
DEPENDENCIES = ["seeed_mr60fda2"]
GetRadarParametersButton = mr60fda2_ns.class_("GetRadarParametersButton", button.Button)
ResetRadarButton = mr60fda2_ns.class_("ResetRadarButton", button.Button)
CONF_GET_RADAR_PARAMETERS = "get_radar_parameters"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_MR60FDA2_ID): cv.use_id(MR60FDA2Component),
cv.Optional(CONF_GET_RADAR_PARAMETERS): button.button_schema(
GetRadarParametersButton,
button.ApplyButton,
device_class=DEVICE_CLASS_UPDATE,
entity_category=ENTITY_CATEGORY_NONE,
),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
ResetRadarButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
@@ -37,5 +35,11 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_MR60FDA2_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_GET_RADAR_PARAMETERS, hub.set_get_radar_parameters_button)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(
CONF_GET_RADAR_PARAMETERS,
hub.set_get_radar_parameters_button,
apply="get_radar_parameters()",
)
await buttons(
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
)
@@ -1,7 +0,0 @@
#include "get_radar_parameters_button.h"
namespace esphome::seeed_mr60fda2 {
void GetRadarParametersButton::press_action() { this->parent_->get_radar_parameters(); }
} // namespace esphome::seeed_mr60fda2
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../seeed_mr60fda2.h"
namespace esphome::seeed_mr60fda2 {
class GetRadarParametersButton final : public button::Button, public Parented<MR60FDA2Component> {
public:
GetRadarParametersButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::seeed_mr60fda2
@@ -1,7 +0,0 @@
#include "reset_radar_button.h"
namespace esphome::seeed_mr60fda2 {
void ResetRadarButton::press_action() { this->parent_->factory_reset(); }
} // namespace esphome::seeed_mr60fda2
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../seeed_mr60fda2.h"
namespace esphome::seeed_mr60fda2 {
class ResetRadarButton final : public button::Button, public Parented<MR60FDA2Component> {
public:
ResetRadarButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::seeed_mr60fda2
+7 -1
View File
@@ -211,9 +211,15 @@ def require_tcp_listener() -> None:
def consume_role_sockets(component: str) -> Callable[[ConfigType], ConfigType]:
"""Socket accounting for a role keyed client or server schema."""
"""Socket accounting for a role keyed client or server schema.
Also requires IPv4, since the TCP link and listener helpers are IPv4 only.
"""
def validator(config: ConfigType) -> ConfigType:
from esphome.components import network
network.require_ipv4(component)(config)
if config[CONF_ROLE] == "server":
consume_sockets(1, component, SocketType.TCP_LISTEN)(config)
return consume_sockets(1, component)(config)
+5
View File
@@ -204,6 +204,11 @@ using socklen_t = uint32_t;
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
// Build tests compile socket without network, which normally defines this
#ifndef USE_NETWORK_IPV4
#define USE_NETWORK_IPV4 1
#endif
namespace esphome::socket {
// Maximum length for formatted socket address string (IP address without port)
+41 -21
View File
@@ -42,54 +42,55 @@ bool socket_ready_fd(int /*fd*/, bool /*loop_monitored*/) { return true; }
#endif
// Platform-specific inet_ntop wrappers
#if USE_NETWORK_IPV4
#if defined(USE_SOCKET_IMPL_LWIP_TCP)
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value
static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) {
inet_ntoa_r(*reinterpret_cast<const struct in_addr *>(addr), buf, size);
return buf;
}
#if USE_NETWORK_IPV6
static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
inet6_ntoa_r(*reinterpret_cast<const ip6_addr_t *>(addr), buf, size);
return buf;
}
#endif
#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
// LWIP sockets (LibreTiny, ESP32 Arduino)
static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) {
return lwip_inet_ntop(AF_INET, addr, buf, size);
}
#if USE_NETWORK_IPV6
static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
return lwip_inet_ntop(AF_INET6, addr, buf, size);
}
#endif
#elif defined(USE_ZEPHYR)
// Zephyr BSD sockets — use Zephyr native address formatting via POSIX-subset wrappers.
// <zephyr/net/socket.h> is already included transitively through <sys/socket.h>.
static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) {
return zsock_inet_ntop(AF_INET, addr, buf, size);
}
// IPv6 is always enabled on nRF52 (config validation enforces enable_ipv6=True),
// but the guard is retained for consistency with other platform blocks.
#if USE_NETWORK_IPV6
static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
return zsock_inet_ntop(AF_INET6, addr, buf, size);
}
#endif
#else
// BSD sockets (host, ESP32-IDF)
static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) {
return inet_ntop(AF_INET, addr, buf, size);
}
#endif
#endif /* USE_NETWORK_IPV4 */
#if USE_NETWORK_IPV6
#if defined(USE_SOCKET_IMPL_LWIP_TCP)
static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
inet6_ntoa_r(*reinterpret_cast<const ip6_addr_t *>(addr), buf, size);
return buf;
}
#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
return lwip_inet_ntop(AF_INET6, addr, buf, size);
}
#elif defined(USE_ZEPHYR)
static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
return zsock_inet_ntop(AF_INET6, addr, buf, size);
}
#else
static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
return inet_ntop(AF_INET6, addr, buf, size);
}
#endif
#endif
#endif /* USE_NETWORK_IPV6 */
bool sockaddr_to_ipv4(const struct sockaddr *addr, uint32_t *out) {
#if USE_NETWORK_IPV4
if (addr->sa_family == AF_INET) {
*out = reinterpret_cast<const struct sockaddr_in *>(addr)->sin_addr.s_addr;
return true;
@@ -105,23 +106,31 @@ bool sockaddr_to_ipv4(const struct sockaddr *addr, uint32_t *out) {
}
}
#endif
#else
(void) addr;
(void) out;
#endif /* USE_NETWORK_IPV4 */
return false;
}
// Format sockaddr into caller-provided buffer, returns length written (excluding null)
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf) {
#if USE_NETWORK_IPV4
if (addr_ptr->sa_family == AF_INET && len >= sizeof(const struct sockaddr_in)) {
const auto *addr = reinterpret_cast<const struct sockaddr_in *>(addr_ptr);
if (esphome_inet_ntop4(&addr->sin_addr, buf.data(), buf.size()) != nullptr)
return strlen(buf.data());
}
#endif /* USE_NETWORK_IPV4 */
#if USE_NETWORK_IPV6
else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) {
if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) {
const auto *addr = reinterpret_cast<const struct sockaddr_in6 *>(addr_ptr);
#if USE_NETWORK_IPV4
uint32_t v4;
// Format a v4 mapped peer as plain IPv4.
if (sockaddr_to_ipv4(addr_ptr, &v4) && esphome_inet_ntop4(&v4, buf.data(), buf.size()) != nullptr)
return strlen(buf.data());
#endif /* USE_NETWORK_IPV4 */
if (esphome_inet_ntop6(&addr->sin6_addr, buf.data(), buf.size()) != nullptr)
return strlen(buf.data());
}
@@ -235,6 +244,10 @@ template<typename F> static bool foreach_eligible_ipv6_if(F &&callback) {
bool join_multicast_group(UDPSocket *sock, const char *ip_address, uint32_t *if_index_out) {
if (strchr(ip_address, ':') == nullptr) {
#if !USE_NETWORK_IPV4
errno = EAFNOSUPPORT;
return false;
#else
struct in_addr ipv4mc{};
#ifdef USE_ZEPHYR
if (zsock_inet_pton(AF_INET, ip_address, &ipv4mc) != 1) {
@@ -262,6 +275,7 @@ bool join_multicast_group(UDPSocket *sock, const char *ip_address, uint32_t *if_
*if_index_out = 0;
}
return true;
#endif // USE_NETWORK_IPV4
}
#ifdef USE_SOCKET_IPV6
struct ipv6_mreq imreq6{};
@@ -373,7 +387,7 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
#else
// Use LWIP-specific functions
ip6_addr_t ip6;
if (inet6_aton(ip_address, &ip6) == 0) {
if (inet6_aton(ip_address, &ip6) != 1) {
errno = EINVAL;
return 0;
}
@@ -382,6 +396,7 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
return sizeof(sockaddr_in6);
}
#endif /* USE_NETWORK_IPV6 */
#if USE_NETWORK_IPV4
if (addrlen < sizeof(sockaddr_in)) {
errno = EINVAL;
return 0;
@@ -404,6 +419,11 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
#endif
server->sin_port = htons(port);
return sizeof(sockaddr_in);
#else
memset(addr, 0, addrlen);
errno = EAFNOSUPPORT;
return 0;
#endif /* USE_NETWORK_IPV4 */
}
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
+2 -1
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import binary_sensor, sensor, socket
from esphome.components import binary_sensor, network, sensor, socket
from esphome.components.const import CONF_HOST
import esphome.config_validation as cv
from esphome.const import (
@@ -48,6 +48,7 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.polling_component_schema("10s")),
network.require_ipv4("statsd"),
socket.consume_sockets(1, "statsd", socket.SocketType.UDP),
)
+5 -7
View File
@@ -14,23 +14,21 @@ from esphome.const import (
import esphome.final_validate as fv
from esphome.types import ConfigType
from .. import CONF_TFLUNA_ID, FACTORY_DEFAULT_ADDRESS, TFLunaComponent, tfluna_ns
from .. import CONF_TFLUNA_ID, FACTORY_DEFAULT_ADDRESS, TFLunaComponent
DEPENDENCIES = ["tfluna"]
ResetButton = tfluna_ns.class_("ResetButton", button.Button)
RestartButton = tfluna_ns.class_("RestartButton", button.Button)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_TFLUNA_ID): cv.use_id(TFLunaComponent),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
ResetButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_RESTART): button.button_schema(
RestartButton,
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_RESTART,
@@ -63,5 +61,5 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_TFLUNA_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_FACTORY_RESET, hub.set_reset_button)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(CONF_FACTORY_RESET, hub.set_reset_button, apply="factory_reset()")
await buttons(CONF_RESTART, hub.set_restart_button, apply="restart()")
@@ -1,7 +0,0 @@
#include "reset_button.h"
namespace esphome::tfluna {
void ResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace esphome::tfluna
@@ -1,13 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../tfluna.h"
namespace esphome::tfluna {
class ResetButton : public button::Button, public Parented<TFLuna> {
protected:
void press_action() override;
};
} // namespace esphome::tfluna
@@ -1,7 +0,0 @@
#include "restart_button.h"
namespace esphome::tfluna {
void RestartButton::press_action() { this->parent_->restart(); }
} // namespace esphome::tfluna
@@ -1,13 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../tfluna.h"
namespace esphome::tfluna {
class RestartButton : public button::Button, public Parented<TFLuna> {
protected:
void press_action() override;
};
} // namespace esphome::tfluna
+10 -14
View File
@@ -4,8 +4,8 @@ from typing import Any
from esphome import automation
from esphome.automation import Trigger
import esphome.codegen as cg
from esphome.components import network
from esphome.components.const import CONF_ON_RECEIVE
from esphome.components.network import DOMAIN as NETWORK_DOMAIN
from esphome.components.packet_transport import (
CONF_BINARY_SENSORS,
CONF_ENCRYPTION,
@@ -15,13 +15,7 @@ from esphome.components.packet_transport import (
CONF_SENSORS,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_DATA,
CONF_ENABLE_IPV6,
CONF_ID,
CONF_PORT,
CONF_TRIGGER_ID,
)
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
@@ -106,7 +100,6 @@ def _final_validate(config: ConfigType) -> None:
from esphome.components import socket
full_config = fv.full_config.get()
ipv6_enabled = full_config.get(NETWORK_DOMAIN, {}).get(CONF_ENABLE_IPV6, False)
raw_lwip = (
full_config.get(socket.DOMAIN, {}).get(socket.CONF_IMPLEMENTATION)
== socket.IMPLEMENTATION_LWIP_TCP
@@ -122,11 +115,6 @@ def _final_validate(config: ConfigType) -> None:
raise cv.Invalid(
f"IPv6 address '{addr}' is not supported on this platform", [key]
)
if not ipv6_enabled:
raise cv.Invalid(
f"IPv6 address '{addr}' requires 'enable_ipv6: true' in the 'network' component",
[key],
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -136,6 +124,12 @@ def _ipv6_addresses(config: ConfigType) -> list[IPv6Address]:
return [addr for addr in config[CONF_ADDRESSES] if addr.version == 6]
def _require_ipv6_for_ipv6_addresses(config: ConfigType) -> ConfigType:
if _ipv6_addresses(config) or config[CONF_LISTEN_ADDRESS].version == 6:
network.require_ipv6("udp")(config)
return config
def _has_ipv4_address(config: ConfigType) -> bool:
return any(addr.version == 4 for addr in config[CONF_ADDRESSES])
@@ -178,6 +172,8 @@ CONFIG_SCHEMA = cv.All(
}
).extend(RELOCATED),
_consume_udp_sockets,
network.require_ipv4("udp"),
_require_ipv6_for_ipv6_addresses,
)
@@ -1,6 +1,12 @@
from esphome import automation, core
import esphome.codegen as cg
from esphome.components import media_player, micro_wake_word, microphone, speaker
from esphome.components import (
media_player,
micro_wake_word,
microphone,
network,
speaker,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
@@ -169,6 +175,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.key_requires((CONF_ON_TTS_STREAM_START, CONF_ON_TTS_STREAM_END), CONF_SPEAKER),
network.require_ipv4("voice_assistant"),
)
FINAL_VALIDATE_SCHEMA = cv.All(
+2 -1
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import button, socket
from esphome.components import button, network, socket
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
@@ -22,6 +22,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_TARGET_MAC_ADDRESS): cv.mac_address,
}
),
network.require_ipv4("wake_on_lan"),
socket.consume_sockets(1, "wake_on_lan", socket.SocketType.UDP),
)
+2
View File
@@ -20,6 +20,7 @@ from esphome.components.network import (
has_high_performance_wifi,
ip_address_literal,
require_high_performance_wifi,
require_ipv4,
)
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
from esphome.config_helpers import filter_source_files_from_platform
@@ -702,6 +703,7 @@ CONFIG_SCHEMA = cv.All(
_apply_min_auth_mode_default,
_validate,
_report_provisioning_credentials,
require_ipv4(DOMAIN),
)
+2 -1
View File
@@ -3,7 +3,7 @@ import re
from esphome import automation
import esphome.codegen as cg
from esphome.components import time
from esphome.components import network, time
from esphome.components.esp32 import CORE, add_idf_sdkconfig_option
import esphome.config_validation as cv
from esphome.const import (
@@ -92,6 +92,7 @@ CONFIG_SCHEMA = cv.All(
PLATFORM_LN882X,
]
),
network.require_ipv4("wireguard"),
)
+7
View File
@@ -241,6 +241,13 @@
#define NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT 1
#define USE_NETWORK
#define USE_NETWORK_DEFAULT_ROUTE
#define USE_NETWORK_IPV4 true
// lwIP is built without IPv6 on ESP8266, RP2 and LibreTiny
#if defined(USE_ESP32) || defined(USE_HOST)
#define USE_NETWORK_IPV6 true
#else
#define USE_NETWORK_IPV6 false
#endif
#define USE_NETWORK_PRIMARY_INTERFACE_WIFI
#define USE_NEXTION_COMMAND_SPACING
#define USE_NEXTION_CONF_START_UP_PAGE
+23 -4
View File
@@ -51,6 +51,11 @@ def _ninja_logs(skip_bootloader: bool) -> list[str]:
BOOTLOADER_BYPRODUCT = re.compile(r"(^|/build/)bootloader/")
# The build step of any CMake ExternalProject. One with BUILD_ALWAYS (ESP-IDF's
# ulp_embed_binary) re-runs on every build, and ninja re-records all of that
# edge's byproducts with the new end time even though the files are untouched.
# Real work on them still changes the hashed firmware.
EXTERNAL_PROJECT_BUILD_STAMP = re.compile(r"-stamp/[^/]+-build$")
MACRO_CHANGED = (
"IDF changed __build_process_project_includes; update "
"IDF_BOOTLOADER_OVERRIDE in esphome/build_gen/espidf.py"
@@ -96,10 +101,24 @@ def _ninja_mtimes(build_path: Path, logs: list[str]) -> dict[tuple[str, str], st
for name in logs:
log = build_path / name
lines = log.read_text(encoding="utf-8").splitlines() if log.is_file() else []
for fields in (line.split("\t") for line in lines if not line.startswith("#")):
if len(fields) >= 4 and (
name != TOP_NINJA_LOG or not BOOTLOADER_BYPRODUCT.search(fields[3])
):
rows = [
fields
for fields in (
line.split("\t") for line in lines if not line.startswith("#")
)
if len(fields) >= 5
]
# Every output of an edge shares its command hash, so the stamp names
# the whole external project group.
external = {
fields[4]
for fields in rows
if EXTERNAL_PROJECT_BUILD_STAMP.search(fields[3])
}
for fields in rows:
if fields[4] in external:
continue
if name != TOP_NINJA_LOG or not BOOTLOADER_BYPRODUCT.search(fields[3]):
mtimes[name, fields[3]] = fields[2]
return mtimes
@@ -6,6 +6,7 @@ from pathlib import Path
import pytest
from esphome.components import network
from esphome.components.api import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
import esphome.config_validation as cv
@@ -134,22 +135,20 @@ def test_outgoing_connection_folds_v4_mapped_host(
assert FINAL_VALIDATE_SCHEMA(config) is not None
def test_outgoing_connection_ipv6_host_requires_ipv6(
set_core_config: SetCoreConfigCallable,
@pytest.mark.parametrize(
("host", "required", "other"),
[
("192.168.1.2", "require_ipv4", "require_ipv6"),
("::ffff:192.168.1.2", "require_ipv4", "require_ipv6"),
("fd00::1", "require_ipv6", "require_ipv4"),
],
)
def test_outgoing_connection_host_requires_its_address_family(
set_core_config: SetCoreConfigCallable, host: str, required: str, other: str
) -> None:
"""The host's family is registered with network, which resolves enable_ipv4/6 later."""
set_core_config(PlatformFramework.ESP32_IDF, platform_data=ESP32_PLATFORM_DATA)
config = CONFIG_SCHEMA(_api_config({"host": "fd00::1"}))
with pytest.raises(cv.Invalid, match="IPv6 is not"):
FINAL_VALIDATE_SCHEMA(config)
def test_outgoing_connection_ipv6_host_passes_with_ipv6_enabled(
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data=ESP32_PLATFORM_DATA,
full_config={"network": {"enable_ipv6": True}},
)
config = CONFIG_SCHEMA(_api_config({"host": "fd00::1"}))
assert FINAL_VALIDATE_SCHEMA(config) is not None
CONFIG_SCHEMA(_api_config({"host": host}))
data = network._get_data()
assert "api.outgoing_connection" in getattr(data, required)
assert "api.outgoing_connection" not in getattr(data, other)
+1
View File
@@ -55,6 +55,7 @@ def reset_core() -> Generator[None]:
"""Reset CORE after each test."""
yield
CORE.reset()
CORE.testing_mode = False # reset() leaves it, so a test enabling it would leak
@pytest.fixture(autouse=True)
@@ -0,0 +1,5 @@
esphome:
name: platformtest
bk72xx:
board: generic-bk7252
@@ -0,0 +1,11 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: arduino
network:
enable_ipv6: true
enable_ipv4: false
@@ -0,0 +1,12 @@
esphome:
name: ardtest
esp32:
board: esp32dev
framework:
type: arduino
network:
enable_ipv6: true
api:
@@ -0,0 +1,7 @@
esphome:
name: platformtest
esp32:
board: esp32dev
framework:
type: arduino
@@ -0,0 +1,5 @@
esphome:
name: platformtest
esp32:
board: esp32dev
@@ -0,0 +1,9 @@
esphome:
name: test
esp8266:
board: esp01_1m
network:
enable_ipv6: true
enable_ipv4: false
@@ -0,0 +1,5 @@
esphome:
name: platformtest
esp8266:
board: esp01_1m
@@ -0,0 +1,9 @@
esphome:
name: hosttest
host:
network:
enable_ipv6: true
api:
@@ -0,0 +1,12 @@
esphome:
name: test
esp32:
variant: esp32c6
framework:
type: esp-idf
udp:
openthread:
tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8
@@ -0,0 +1,14 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
network:
enable_ipv6: true
wifi:
ssid: "test_ssid"
password: "test_password"
@@ -0,0 +1,10 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
network:
enable_ipv6: false
@@ -0,0 +1,10 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
network:
enable_ipv6: true
@@ -0,0 +1,11 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
network:
enable_ipv6: true
enable_ipv4: false
@@ -0,0 +1,10 @@
esphome:
name: nrf52test
nrf52:
board: adafruit_itsybitsy_nrf52840
bootloader: adafruit_nrf52_sd140_v6
network:
api:
@@ -0,0 +1,6 @@
esphome:
name: platformtest
nrf52:
board: adafruit_itsybitsy_nrf52840
bootloader: adafruit_nrf52_sd140_v6
@@ -0,0 +1,13 @@
esphome:
name: test
esp32:
variant: esp32c6
framework:
type: esp-idf
network:
enable_ipv4: true
openthread:
tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8
Loaded 100 of 203 files, more files were not shown because too many files have changed in this diff. Show more