mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 13:56:10 +00:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c336736864 | ||
|
|
bdd5496273 | ||
|
|
35883270fe | ||
|
|
da37e89b9d | ||
|
|
32bbf55f2a | ||
|
|
16759c848f | ||
|
|
9db6c11023 | ||
|
|
e3979f0bbe | ||
|
|
34ebfe3ea3 | ||
|
|
69877018e2 | ||
|
|
909ff7940a | ||
|
|
dee419f655 | ||
|
|
244e419f82 | ||
|
|
04f53f0ab4 | ||
|
|
28b2754b21 | ||
|
|
ecf13e47d3 | ||
|
|
3843e41307 | ||
|
|
9eef601f99 | ||
|
|
454dbe59db | ||
|
|
67ce58bba4 | ||
|
|
fab5b21d0d | ||
|
|
480edb193d | ||
|
|
9c0b53c4ae | ||
|
|
950560f5e4 | ||
|
|
f85da6010e | ||
|
|
ff2c86ba7f | ||
|
|
9b0abf4882 | ||
|
|
a6750951c4 | ||
|
|
568ea8e111 | ||
|
|
a1c4f9a8db | ||
|
|
b02da099a7 | ||
|
|
53e75b6e01 | ||
|
|
f430582fe3 | ||
|
|
0335e6d071 | ||
|
|
616d20fd9e |
@@ -205,14 +205,15 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
for a table of structs copy the whole entry out and return the copy, never a pointer into flash.
|
||||
On other platforms they are a plain `memcpy` and dereference, so no `#ifdef` is needed. Declare log tags with
|
||||
`ESPHOME_LOG_TAG(TAG, "name")`, pass status text as `LOG_STR`, e.g.
|
||||
`this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL))`, and give an action whose payload is a byte array
|
||||
a `TEMPLATABLE_BYTES` field so a static code stays in flash.
|
||||
`this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL))`. An action that only forwards a byte payload is
|
||||
registered with `automation.ApplyBytes` (see Actions below), which keeps a constant payload in flash;
|
||||
`TEMPLATABLE_BYTES` is for a `play()` that does more with the bytes.
|
||||
* **Fixed sets of names** (status words, model names): a `PROGMEM_STRING_TABLE` (`esphome/core/progmem.h`),
|
||||
logged with `get_log_str` and `LOG_STR_ARG`, copied out with `ESPHOME_strncpy_P` before `publish_state`.
|
||||
A `dump_config` helper that picks a name returns `const LogString *` from `LOG_STR(...)`.
|
||||
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `xor_bytes`, `sum16_bytes`,
|
||||
`reverse_bits`, `encode_uint16`, `encode_value_le`, `sign_extend<BITS>`, `bcd_to_uint8`, `assign_bits`); pick
|
||||
the helper's arguments instead of writing a loop. Epson style 7-byte RTC calendars (one-hot weekday) use
|
||||
`reverse_bits`, `encode_uint16`, `encode_value_le`, `sign_extend<BITS>`, `bcd_to_uint8`); pick the helper's
|
||||
arguments instead of writing a loop. Epson style 7-byte RTC calendars (one-hot weekday) use
|
||||
`time::decode_bcd_calendar`.
|
||||
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
|
||||
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
|
||||
@@ -604,7 +605,9 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
)
|
||||
```
|
||||
The `ApplyField`, `ApplyCall` and `register_apply_action` docstrings in `esphome/automation.py` cover
|
||||
the rest; `cover.control` and `cover.template.publish` are in-tree examples. `TEMPLATABLE_VALUE` with
|
||||
the rest; `cover.control` and `cover.template.publish` are in-tree examples. An action whose input is a
|
||||
byte payload passes `automation.ApplyBytes(CONF_DATA, "send({})")` in the same call; a constant payload
|
||||
stays in flash and a lambda's vector is forwarded as data and size. `TEMPLATABLE_VALUE` with
|
||||
`cg.templatable` stays for actions whose `play()` has real logic beyond forwarding values.
|
||||
|
||||
* **Conditions:**
|
||||
|
||||
+107
-14
@@ -59,10 +59,15 @@ async def templatable_bytes(
|
||||
if cg.is_template(value):
|
||||
fn = await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(set_template(fn))
|
||||
elif len(value) > 0xFFFF:
|
||||
raise EsphomeError(f"Byte payload is {len(value)} bytes; the maximum is 65535")
|
||||
else:
|
||||
cg.add(set_static(progmem_bytes(table_name, value), len(value)))
|
||||
cg.add(set_static(*_static_bytes(table_name, value)))
|
||||
|
||||
|
||||
def _static_bytes(table_name: str, value: bytes | list[int]) -> tuple[MockObj, int]:
|
||||
"""The shared PROGMEM table for a constant payload and its length."""
|
||||
if (size := len(value)) > 0xFFFF:
|
||||
raise EsphomeError(f"Byte payload is {size} bytes; the maximum is 65535")
|
||||
return progmem_bytes(table_name, value), size
|
||||
|
||||
|
||||
def maybe_simple_id(*validators):
|
||||
@@ -356,6 +361,33 @@ class ApplyField:
|
||||
return ApplyCall(target, ((self.conf_key, self.type_, self.const_fn),))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApplyBytes:
|
||||
"""A byte payload key forwarded as ``target``, whose ``{}`` stands for ``data, len``.
|
||||
|
||||
A constant payload is a shared PROGMEM table passed as is; on ESP8266 it is first copied
|
||||
out, to the stack up to ``stack`` bytes and to the heap beyond, unless ``progmem_target``
|
||||
names a call that reads flash itself. A lambda's ``std::vector<uint8_t>`` is passed as
|
||||
its data and size. The template may name the parent as ``{parent}``. An absent key
|
||||
emits nothing.
|
||||
"""
|
||||
|
||||
conf_key: str | tuple[str, ...]
|
||||
target: str
|
||||
stack: int = 64
|
||||
progmem_target: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
for target in (self.target, self.progmem_target):
|
||||
if target is None:
|
||||
continue
|
||||
fields = [
|
||||
f for _, f, _, _ in string.Formatter().parse(target) if f is not None
|
||||
]
|
||||
if fields.count("") != 1 or any(f not in ("", "parent") for f in fields):
|
||||
raise ValueError(f"apply bytes target {target!r} needs one {{}}")
|
||||
|
||||
|
||||
def _config_lookup(config: ConfigType, key: str | tuple[str, ...]) -> Any:
|
||||
if isinstance(key, str):
|
||||
return config.get(key)
|
||||
@@ -488,7 +520,7 @@ def _apply_values(config: ConfigType, members: list[tuple[Any, Any, Any]]) -> li
|
||||
def register_apply_action(
|
||||
name: str,
|
||||
schema: cv.Schema,
|
||||
*fields: ApplyField | ApplyCall,
|
||||
*fields: ApplyField | ApplyCall | ApplyBytes,
|
||||
call: str | None = None,
|
||||
id_key: str = CONF_ID,
|
||||
) -> None:
|
||||
@@ -504,7 +536,7 @@ def register_apply_action(
|
||||
def apply_action_builder(
|
||||
name: str,
|
||||
schema: cv.Schema,
|
||||
*fields: ApplyField | ApplyCall,
|
||||
*fields: ApplyField | ApplyCall | ApplyBytes,
|
||||
call: str | None = None,
|
||||
id_key: str = CONF_ID,
|
||||
) -> Callable[..., Awaitable[MockObj]]:
|
||||
@@ -517,17 +549,20 @@ def apply_action_builder(
|
||||
and ``apply_call.perform()`` is appended.
|
||||
"""
|
||||
# An action stores the value, so a std::string constant stays in flash on ESP8266.
|
||||
statements_spec = [
|
||||
(
|
||||
c.target,
|
||||
_default_std_string(c.members, flash_string),
|
||||
)
|
||||
for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields)
|
||||
]
|
||||
_check_key_in_schema(name, schema, id_key)
|
||||
for _, members in statements_spec:
|
||||
statements_spec: list[ApplyBytes | tuple[str, list[tuple[Any, Any, Any]]]] = []
|
||||
for f in fields:
|
||||
if isinstance(f, ApplyBytes):
|
||||
_check_key_in_schema(name, schema, f.conf_key)
|
||||
statements_spec.append(f)
|
||||
continue
|
||||
c = f if isinstance(f, ApplyCall) else f.call()
|
||||
members = _default_std_string(c.members, flash_string)
|
||||
for conf_key, _, _ in members:
|
||||
_check_key_in_schema(name, schema, conf_key)
|
||||
statements_spec.append((c.target, members))
|
||||
# Named like the generated function, so it cannot clash with an SDK symbol
|
||||
table_name = f"esphome__{name.replace('.', '_')}__bytes"
|
||||
|
||||
async def builder(
|
||||
config: ConfigType,
|
||||
@@ -539,7 +574,15 @@ def apply_action_builder(
|
||||
lambda_args = _apply_lambda_args(args)
|
||||
receiver = "apply_call." if call else f"{parent}->"
|
||||
statements: list[str] = []
|
||||
for target, members in statements_spec:
|
||||
for spec in statements_spec:
|
||||
if isinstance(spec, ApplyBytes):
|
||||
statements.extend(
|
||||
await _render_bytes(
|
||||
spec, config, receiver, parent, lambda_args, table_name
|
||||
)
|
||||
)
|
||||
continue
|
||||
target, members = spec
|
||||
values = _apply_values(config, members)
|
||||
if not _apply_call_active(members, values):
|
||||
continue
|
||||
@@ -560,6 +603,56 @@ def apply_action_builder(
|
||||
return builder
|
||||
|
||||
|
||||
async def _render_bytes(
|
||||
spec: ApplyBytes,
|
||||
config: ConfigType,
|
||||
receiver: str,
|
||||
parent: str,
|
||||
lambda_args: TemplateArgsType,
|
||||
table_name: str,
|
||||
) -> list[str]:
|
||||
"""The statements for one ``ApplyBytes``: what ``TemplatableBytes::visit`` does, inlined."""
|
||||
value = _config_lookup(config, spec.conf_key)
|
||||
if value is None:
|
||||
return []
|
||||
|
||||
# The temporary carries the generated-name prefix so a trigger argument cannot shadow it
|
||||
def send(target: str, data: str) -> str:
|
||||
return f"{receiver}{target.format(data, parent=parent)};"
|
||||
|
||||
if isinstance(value, Lambda):
|
||||
inner = await cg.process_lambda(
|
||||
value, lambda_args, return_type=cg.std_vector.template(cg.uint8)
|
||||
)
|
||||
return [
|
||||
"{",
|
||||
f" const std::vector<uint8_t> esphome__bytes = ({call_lambda(inner)});",
|
||||
f" {send(spec.target, 'esphome__bytes.data(), esphome__bytes.size()')}",
|
||||
"}",
|
||||
]
|
||||
table, size = _static_bytes(table_name, value)
|
||||
# Qualified so a trigger argument cannot shadow a reused table; an empty payload is nullptr
|
||||
args = f"::{table}, {size}" if size else f"{table}, {size}"
|
||||
if CORE.is_esp8266 and spec.progmem_target is not None:
|
||||
return [send(spec.progmem_target, args)]
|
||||
if not CORE.is_esp8266:
|
||||
return [send(spec.target, args)]
|
||||
# A PROGMEM table must be read with aligned loads on ESP8266, so copy it out first;
|
||||
# the length is known here, so the copy is an exact array, on the heap past ``stack``.
|
||||
# An empty payload still gets a one byte array: the raw lwIP socket rejects a null buffer.
|
||||
if size <= spec.stack:
|
||||
buffer, data = f"uint8_t esphome__bytes[{max(size, 1)}];", "esphome__bytes"
|
||||
else:
|
||||
buffer, data = (
|
||||
f"auto esphome__bytes = std::make_unique<uint8_t[]>({size});",
|
||||
"esphome__bytes.get()",
|
||||
)
|
||||
statements = ["{", f" {buffer}"]
|
||||
if size:
|
||||
statements.append(f" esphome::progmem_memcpy({data}, {args});")
|
||||
return [*statements, f" {send(spec.target, f'{data}, {size}')}", "}"]
|
||||
|
||||
|
||||
def _apply_call_active(members: list[tuple[Any, Any, Any]], values: list[Any]) -> bool:
|
||||
"""An ``ApplyCall`` is emitted unless it has keys and none of them is set."""
|
||||
return not members or any(value is not None for value in values)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -295,11 +295,6 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
|
||||
# Actions
|
||||
SendPacketAction = ns.class_(
|
||||
"SendPacketAction", automation.Action, cg.Parented.template(CC1101Component)
|
||||
)
|
||||
|
||||
CC1101_ACTION_SCHEMA = cv.Schema(
|
||||
maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)})
|
||||
)
|
||||
@@ -338,23 +333,11 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"cc1101.send_packet",
|
||||
SendPacketAction,
|
||||
SEND_PACKET_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyBytes(CONF_DATA, "transmit_packet({})"),
|
||||
)
|
||||
async def send_packet_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA],
|
||||
args,
|
||||
var.set_data_template,
|
||||
var.set_data_static,
|
||||
"cc1101_data",
|
||||
)
|
||||
return var
|
||||
|
||||
|
||||
_CONFIG_VALIDATORS = {opt.schema: validator for opt, validator in CONFIG_MAP.items()}
|
||||
|
||||
@@ -132,13 +132,4 @@ class CC1101Component final : public Component,
|
||||
bool enter_tx_();
|
||||
};
|
||||
|
||||
// Action Wrappers
|
||||
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->data_.visit([this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::cc1101
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "ch422g.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ch422g {
|
||||
@@ -60,7 +59,11 @@ bool CH422GComponent::digital_read(uint8_t pin) {
|
||||
}
|
||||
|
||||
void CH422GComponent::digital_write(uint8_t pin, bool value) {
|
||||
assign_bits(this->output_bits_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_bits_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_bits_ &= ~(1 << pin);
|
||||
}
|
||||
this->write_outputs_();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "ch423.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
@@ -63,7 +62,11 @@ bool CH423Component::digital_read(uint8_t pin) {
|
||||
}
|
||||
|
||||
void CH423Component::digital_write(uint8_t pin, bool value) {
|
||||
assign_bits(this->output_bits_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_bits_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_bits_ &= ~(1 << pin);
|
||||
}
|
||||
this->write_outputs_();
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "mcp23016.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstdio>
|
||||
|
||||
@@ -80,7 +79,11 @@ void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) {
|
||||
this->read_reg_(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
assign_bits(reg_value, 1 << pin, pin_value);
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << pin;
|
||||
} else {
|
||||
reg_value &= ~(1 << pin);
|
||||
}
|
||||
|
||||
this->write_reg_(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "mcp23x08_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::mcp23x08_base {
|
||||
@@ -79,7 +78,11 @@ void MCP23X08Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
|
||||
this->read_reg(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
assign_bits(reg_value, 1 << bit, pin_value);
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << bit;
|
||||
} else {
|
||||
reg_value &= ~(1 << bit);
|
||||
}
|
||||
|
||||
this->write_reg(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -92,7 +92,11 @@ void MCP23X17Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
|
||||
this->read_reg(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
assign_bits(reg_value, 1 << bit, pin_value);
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << bit;
|
||||
} else {
|
||||
reg_value &= ~(1 << bit);
|
||||
}
|
||||
|
||||
this->write_reg(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pca6416a.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca6416a {
|
||||
@@ -171,7 +170,11 @@ void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t re
|
||||
this->read_register_(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
assign_bits(reg_value, 1 << bit, pin_value);
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << bit;
|
||||
} else {
|
||||
reg_value &= ~(1 << bit);
|
||||
}
|
||||
|
||||
this->write_register_(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pca9505.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca9505 {
|
||||
@@ -75,7 +74,11 @@ bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin
|
||||
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
const uint8_t bank = pin / 8;
|
||||
const uint8_t mask = 1 << (pin % 8);
|
||||
assign_bits(this->output_[bank], mask, value);
|
||||
if (value) {
|
||||
this->output_[bank] |= mask;
|
||||
} else {
|
||||
this->output_[bank] &= ~mask;
|
||||
}
|
||||
this->write_bank_(OUTPUT_REG, bank, this->output_[bank]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pca9554.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca9554 {
|
||||
@@ -100,7 +99,11 @@ bool PCA9554Component::digital_read_cache(uint8_t pin) {
|
||||
}
|
||||
|
||||
void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
if (this->write_register_(OUTPUT_REG, this->output_mask_))
|
||||
this->retry_pcal_shadows_(); // an output-only hub never reads, so it retries here
|
||||
|
||||
@@ -60,7 +60,11 @@ bool PCF8574Component::digital_read_hw(uint8_t pin) {
|
||||
bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
|
||||
|
||||
void PCF8574Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
this->write_gpio_();
|
||||
}
|
||||
void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pi4ioe5v6408.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pi4ioe5v6408 {
|
||||
@@ -130,7 +129,11 @@ void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
if (!this->status_comm_result(this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_),
|
||||
LOG_STR("Failed to write output register")))
|
||||
return;
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@ from esphome.components.const import (
|
||||
CONF_RST_PIN,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import ID, TimePeriod
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY
|
||||
from esphome.core import TimePeriod
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DOMAIN = "sx126x"
|
||||
MULTI_CONF = True
|
||||
@@ -133,10 +132,6 @@ SHAPING = {
|
||||
"NONE": SX126xPulseShape.NO_FILTER,
|
||||
}
|
||||
|
||||
SendPacketAction = sx126x_ns.class_(
|
||||
"SendPacketAction", automation.Action, cg.Parented.template(SX126x)
|
||||
)
|
||||
|
||||
|
||||
def validate_raw_data(value: Any) -> bytes | list[int]:
|
||||
if isinstance(value, str):
|
||||
@@ -324,25 +319,8 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"sx126x.send_packet",
|
||||
SendPacketAction,
|
||||
SEND_PACKET_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyBytes(CONF_DATA, "transmit_packet({})", stack=MAX_PACKET_SIZE),
|
||||
)
|
||||
async def send_packet_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA],
|
||||
args,
|
||||
var.set_data_template,
|
||||
var.set_data_static,
|
||||
"sx126x_data",
|
||||
)
|
||||
return var
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sx126x/sx126x.h"
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<SX126x> {
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->data_.template visit<SX126X_MAX_PACKET_SIZE>(
|
||||
[this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -11,9 +11,7 @@ from esphome.components.const import (
|
||||
CONF_RST_PIN,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.const import CONF_DATA, CONF_FREQUENCY
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DOMAIN = "sx127x"
|
||||
@@ -117,10 +115,6 @@ SHAPING = {
|
||||
"NONE": SX127xPaRamp.SHAPING_NONE,
|
||||
}
|
||||
|
||||
SendPacketAction = sx127x_ns.class_(
|
||||
"SendPacketAction", automation.Action, cg.Parented.template(SX127x)
|
||||
)
|
||||
|
||||
|
||||
def validate_raw_data(value: Any) -> bytes | list[int]:
|
||||
if isinstance(value, str):
|
||||
@@ -291,25 +285,8 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"sx127x.send_packet",
|
||||
SendPacketAction,
|
||||
SEND_PACKET_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyBytes(CONF_DATA, "transmit_packet({})", stack=MAX_PACKET_SIZE),
|
||||
)
|
||||
async def send_packet_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA],
|
||||
args,
|
||||
var.set_data_template,
|
||||
var.set_data_static,
|
||||
"sx127x_data",
|
||||
)
|
||||
return var
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sx127x/sx127x.h"
|
||||
|
||||
namespace esphome::sx127x {
|
||||
|
||||
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<SX127x> {
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
public:
|
||||
void play(const Ts &...x) override {
|
||||
this->data_.template visit<SX127X_MAX_PACKET_SIZE>(
|
||||
[this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::sx127x
|
||||
@@ -92,9 +92,20 @@ void SX1509Component::digital_write_hw(uint8_t pin, bool bit_value) {
|
||||
// If the pin is an output, write high/low
|
||||
uint16_t temp_reg_data = 0;
|
||||
this->read_byte_16(REG_DATA_B, &temp_reg_data);
|
||||
assign_bits(this->output_state_, 1 << pin, bit_value);
|
||||
// Outputs take the shadow state, inputs keep what was read
|
||||
temp_reg_data = (temp_reg_data & this->ddr_mask_) | (this->output_state_ & ~this->ddr_mask_);
|
||||
if (bit_value) {
|
||||
output_state_ |= (1 << pin); // set bit in shadow register
|
||||
} else {
|
||||
output_state_ &= ~(1 << pin); // reset bit shadow register
|
||||
}
|
||||
for (uint16_t b = 0x8000; b; b >>= 1) {
|
||||
if ((~ddr_mask_) & b) { // transfer bits of outputs, but don't mess with inputs
|
||||
if (output_state_ & b) {
|
||||
temp_reg_data |= b;
|
||||
} else {
|
||||
temp_reg_data &= ~b;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->write_byte_16(REG_DATA_B, temp_reg_data);
|
||||
}
|
||||
}
|
||||
@@ -137,12 +148,20 @@ void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
|
||||
// Pullup
|
||||
this->read_byte_16(REG_PULL_UP_B, &temp_word);
|
||||
assign_bits(temp_word, 1 << pin, flags & gpio::FLAG_PULLUP);
|
||||
if (flags & gpio::FLAG_PULLUP) {
|
||||
temp_word |= (1 << pin);
|
||||
} else {
|
||||
temp_word &= ~(1 << pin);
|
||||
}
|
||||
this->write_byte_16(REG_PULL_UP_B, temp_word);
|
||||
|
||||
// Pulldown
|
||||
this->read_byte_16(REG_PULL_DOWN_B, &temp_word);
|
||||
assign_bits(temp_word, 1 << pin, flags & gpio::FLAG_PULLDOWN);
|
||||
if (flags & gpio::FLAG_PULLDOWN) {
|
||||
temp_word |= (1 << pin);
|
||||
} else {
|
||||
temp_word &= ~(1 << pin);
|
||||
}
|
||||
this->write_byte_16(REG_PULL_DOWN_B, temp_word);
|
||||
|
||||
// Set direction to input
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "tca9555.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
static const uint8_t TCA9555_INPUT_PORT_REGISTER_0 = 0x00;
|
||||
@@ -109,7 +108,11 @@ void TCA9555Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
|
||||
this->status_comm_result(this->write_byte_16_le(TCA9555_OUTPUT_PORT_REGISTER_0, this->output_mask_),
|
||||
LOG_STR("Failed to write output register"));
|
||||
|
||||
@@ -121,7 +121,6 @@ HOST_BAUD_RATES = [
|
||||
]
|
||||
|
||||
UARTDevice = uart_ns.class_("UARTDevice")
|
||||
UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action)
|
||||
UARTDebugger = uart_ns.class_("UARTDebugger", cg.Component, automation.Action)
|
||||
UARTDummyReceiver = uart_ns.class_("UARTDummyReceiver", cg.Component)
|
||||
MULTI_CONF = True
|
||||
@@ -593,9 +592,8 @@ def new_uart_component(
|
||||
return cg.new_bus_component(register_uart_device, config, *args)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"uart.write",
|
||||
UARTWriteAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(UARTComponent),
|
||||
@@ -603,15 +601,11 @@ def new_uart_component(
|
||||
},
|
||||
key=CONF_DATA,
|
||||
),
|
||||
synchronous=True,
|
||||
# A constant stays in flash: write_array_progmem copies it out in chunks on ESP8266
|
||||
automation.ApplyBytes(
|
||||
CONF_DATA, "write_array({})", progmem_target="write_array_progmem({})"
|
||||
),
|
||||
)
|
||||
async def uart_write_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA], args, var.set_data_template, var.set_data_static, "uart_data"
|
||||
)
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
template<typename... Ts> class UARTWriteAction final : public Action<Ts...>, public Parented<UARTComponent> {
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->data_.is_static()) {
|
||||
this->parent_->write_array_progmem(this->data_.data(), this->data_.size());
|
||||
} else {
|
||||
this->parent_->write_array(this->data_.value(x...));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::uart
|
||||
@@ -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
|
||||
@@ -35,7 +29,6 @@ DOMAIN = "udp"
|
||||
MULTI_CONF = True
|
||||
udp_ns = cg.esphome_ns.namespace("udp")
|
||||
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
|
||||
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
|
||||
trigger_argname = "data"
|
||||
# Listener callback type (non-owning span from UDP component)
|
||||
listener_args = cg.std_span.template(cg.uint8.operator("const"))
|
||||
@@ -106,7 +99,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 +114,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 +123,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 +171,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
).extend(RELOCATED),
|
||||
_consume_udp_sockets,
|
||||
network.require_ipv4("udp"),
|
||||
_require_ipv6_for_ipv6_addresses,
|
||||
)
|
||||
|
||||
|
||||
@@ -244,17 +239,23 @@ def validate_raw_data(value: Any) -> bytes | list[int]:
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
_UDP_WRITE_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(UDPComponent),
|
||||
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
|
||||
},
|
||||
key=CONF_DATA,
|
||||
)
|
||||
_udp_write_builder = automation.apply_action_builder(
|
||||
"udp.write",
|
||||
UDPWriteAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(UDPComponent),
|
||||
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
|
||||
},
|
||||
key=CONF_DATA,
|
||||
),
|
||||
synchronous=True,
|
||||
_UDP_WRITE_SCHEMA,
|
||||
# Constant payloads up to this size are sent from the stack on ESP8266
|
||||
automation.ApplyBytes(CONF_DATA, "send_packet({})", stack=128),
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"udp.write", automation.ApplyAction, _UDP_WRITE_SCHEMA, synchronous=True
|
||||
)
|
||||
async def udp_write_to_code(
|
||||
config: ConfigType,
|
||||
@@ -262,11 +263,6 @@ async def udp_write_to_code(
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
udp_var = await cg.get_variable(config[CONF_ID])
|
||||
await cg.register_parented(var, udp_var)
|
||||
cg.add(udp_var.set_should_broadcast())
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA], args, var.set_data_template, var.set_data_static, "udp_data"
|
||||
)
|
||||
return var
|
||||
return await _udp_write_builder(config, action_id, template_arg, args)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "udp_component.h"
|
||||
#ifdef USE_NETWORK
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
namespace esphome::udp {
|
||||
|
||||
/// Constant payloads up to this size are sent from the stack on ESP8266.
|
||||
static constexpr size_t STACK_PAYLOAD_SIZE = 128;
|
||||
|
||||
template<typename... Ts> class UDPWriteAction final : public Action<Ts...>, public Parented<UDPComponent> {
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
// One sendto per packet, so a failed allocation never sends a partial packet
|
||||
this->data_.template visit<STACK_PAYLOAD_SIZE>(
|
||||
[this](const uint8_t *data, size_t len) { this->parent_->send_packet(data, len); }, x...);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::udp
|
||||
|
||||
#endif
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "waveshare_io_ch32v003.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::waveshare_io_ch32v003 {
|
||||
@@ -111,7 +110,11 @@ void WaveshareIOCH32V003Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
|
||||
uint8_t data = this->output_mask_;
|
||||
this->status_comm_result(this->write_byte(IO_EXTENSION_IO_OUTPUT_ADDR, data),
|
||||
|
||||
@@ -210,7 +210,11 @@ bool WeikaiComponent::read_pin_val_(uint8_t pin) {
|
||||
}
|
||||
|
||||
void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) {
|
||||
assign_bits(this->output_state_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_state_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_state_ &= ~(1 << pin);
|
||||
}
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value),
|
||||
format_bin_to(bin_buf, this->output_state_));
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1067,16 +1067,6 @@ constexpr uint8_t bcd_to_uint8(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x
|
||||
/// Encode a value from 0 to 99 as a two-digit packed BCD byte, e.g. 59 to 0x59.
|
||||
constexpr uint8_t uint8_to_bcd(uint8_t value) { return ((value / 10) << 4) | (value % 10); }
|
||||
|
||||
/// Set the bits of \p mask in \p value when \p on is true, clear them otherwise.
|
||||
/// Use an unsigned mask for a register wider than int; a negative int mask is sign-extended.
|
||||
template<typename T> ESPHOME_ALWAYS_INLINE constexpr void assign_bits(T &value, std::type_identity_t<T> mask, bool on) {
|
||||
if (on) {
|
||||
value |= mask;
|
||||
} else {
|
||||
value &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverse the order of 8 bits.
|
||||
inline uint8_t reverse_bits(uint8_t x) {
|
||||
x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_constant_payloads_share_progmem_tables(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Equal send_packet payloads share one table; lambdas stay templates."""
|
||||
"""Equal send_packet payloads share one table; a lambda is sent from its vector."""
|
||||
main_cpp = generate_main(component_config_path("payload_tables.yaml"))
|
||||
|
||||
tables = re.findall(
|
||||
@@ -23,8 +23,10 @@ def test_constant_payloads_share_progmem_tables(
|
||||
assert [v for _, v in tables] == [
|
||||
"{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}"
|
||||
]
|
||||
assert main_cpp.count(f"set_data_static({tables[0][0]}, 8);") == 2
|
||||
assert "set_data_template(" in main_cpp
|
||||
assert main_cpp.count(f"->transmit_packet(::{tables[0][0]}, 8);") == 2
|
||||
assert (
|
||||
"->transmit_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [64, 65])
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
openthread:
|
||||
tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
openthread:
|
||||
tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8
|
||||
|
||||
button:
|
||||
- platform: wake_on_lan
|
||||
name: Wake PC
|
||||
target_mac_address: AA:BB:CC:DD:EE:FF
|
||||
@@ -0,0 +1,5 @@
|
||||
esphome:
|
||||
name: platformtest
|
||||
|
||||
rp2:
|
||||
board: rpipicow
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
openthread:
|
||||
tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8
|
||||
|
||||
network:
|
||||
enable_ipv4: false
|
||||
|
||||
tcp_uart:
|
||||
- id: tcp_uart_server
|
||||
role: server
|
||||
port: 5020
|
||||
@@ -0,0 +1,25 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
openthread:
|
||||
tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8
|
||||
|
||||
network:
|
||||
enable_ipv4: false
|
||||
|
||||
uart:
|
||||
id: uart_bus
|
||||
tx_pin: 4
|
||||
rx_pin: 5
|
||||
baud_rate: 115200
|
||||
|
||||
uart_tcp:
|
||||
- id: bridge
|
||||
uart_id: uart_bus
|
||||
role: server
|
||||
port: 502
|
||||
@@ -0,0 +1,16 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
enable_ipv4: false
|
||||
|
||||
udp:
|
||||
id: my_udp
|
||||
listen_address: 239.0.60.53
|
||||
addresses: ["239.0.60.53"]
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
openthread:
|
||||
tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8
|
||||
|
||||
network:
|
||||
enable_ipv4: false
|
||||
|
||||
button:
|
||||
- platform: wake_on_lan
|
||||
name: Wake PC
|
||||
target_mac_address: AA:BB:CC:DD:EE:FF
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
enable_ipv4: false
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
@@ -0,0 +1,191 @@
|
||||
"""Tests for network component."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from esphome.components.network import (
|
||||
CONF_ENABLE_IPV4,
|
||||
_final_validate,
|
||||
_get_data,
|
||||
request_ipv4_off,
|
||||
require_ipv4,
|
||||
require_ipv6,
|
||||
)
|
||||
from esphome.config import load_config
|
||||
from esphome.const import CONF_ENABLE_IPV6, KEY_FRAMEWORK_VERSION, PlatformFramework
|
||||
from esphome.core import CORE, Version
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _define_value(name: str) -> str | None:
|
||||
define = next((d for d in CORE.defines if d.name == name), None)
|
||||
return None if define is None else str(define.value)
|
||||
|
||||
|
||||
def test_require_ipv4_records_name_and_passes_config_through() -> None:
|
||||
"""require_ipv4() returns the config unchanged and records the caller once."""
|
||||
config = {"foo": "bar"}
|
||||
validator = require_ipv4("foo")
|
||||
assert validator(config) is config
|
||||
validator({})
|
||||
assert _get_data().require_ipv4 == {"foo"}
|
||||
|
||||
|
||||
def test_require_ipv6_and_request_ipv4_off_record_state() -> None:
|
||||
"""require_ipv6() and request_ipv4_off() record their callers."""
|
||||
require_ipv6("bar")({})
|
||||
request_ipv4_off("bar")({})
|
||||
data = _get_data()
|
||||
assert data.require_ipv6 == {"bar"}
|
||||
assert data.request_ipv4_off == {"bar"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "ipv4", "ipv6"),
|
||||
[
|
||||
("ipv6_only_ipv4_explicit_disable.yaml", "false", "true"),
|
||||
("ipv6_only.yaml", "true", "true"),
|
||||
("ipv4_required_with_ipv6.yaml", "true", "true"),
|
||||
("wifi_only.yaml", "true", "false"),
|
||||
("openthread_no_explicit_network.yaml", "false", "true"),
|
||||
("openthread_ipv4_explicitly_on.yaml", "true", "true"),
|
||||
("ipv6_off_nothing_required.yaml", "true", "false"),
|
||||
("ipv4_and_ipv6_both_required.yaml", "true", "true"),
|
||||
("openthread_with_wake_on_lan.yaml", "true", "true"),
|
||||
("esp32_arduino_ipv6_no_ipv4_requirement.yaml", "true", "true"),
|
||||
("host_ipv6_no_ipv4_requirement.yaml", "true", "true"),
|
||||
("nrf52_network_no_openthread.yaml", "false", "true"),
|
||||
],
|
||||
)
|
||||
def test_ip_family_defines(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
ipv4: str,
|
||||
ipv6: str,
|
||||
) -> None:
|
||||
"""Each config resolves USE_NETWORK_IPV4 and USE_NETWORK_IPV6 as expected."""
|
||||
generate_main(component_config_path(config_file))
|
||||
assert _define_value("USE_NETWORK_IPV4") == ipv4
|
||||
assert _define_value("USE_NETWORK_IPV6") == ipv6
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config_file",
|
||||
[
|
||||
"wifi_ipv4_disabled_rejected.yaml",
|
||||
"wake_on_lan_ipv4_disabled_rejected.yaml",
|
||||
"udp_ipv4_disabled_rejected.yaml",
|
||||
"tcp_uart_ipv4_disabled_rejected.yaml",
|
||||
"uart_tcp_ipv4_disabled_rejected.yaml",
|
||||
"esp8266_ipv4_disabled_rejected.yaml",
|
||||
"esp32_arduino_ipv4_disabled_rejected.yaml",
|
||||
],
|
||||
)
|
||||
def test_require_ipv4_registration_rejects_explicit_disable(
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
) -> None:
|
||||
"""A component that requires IPv4 rejects an explicit 'enable_ipv4: false'."""
|
||||
CORE.config_path = component_config_path(config_file)
|
||||
res = load_config({})
|
||||
assert any("explicitly disables it" in str(err) for err in res.errors)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "component"),
|
||||
[
|
||||
("tcp_uart_ipv4_disabled_rejected.yaml", "tcp_uart"),
|
||||
("uart_tcp_ipv4_disabled_rejected.yaml", "uart_tcp"),
|
||||
],
|
||||
)
|
||||
def test_tcp_link_users_named_in_ipv4_error(
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
component: str,
|
||||
) -> None:
|
||||
"""The IPv4 requirement registered by the socket helper names the component."""
|
||||
CORE.config_path = component_config_path(config_file)
|
||||
res = load_config({})
|
||||
assert any(f"IPv4 is required by {component}" in str(err) for err in res.errors)
|
||||
|
||||
|
||||
def test_final_validate_rejects_explicit_ipv6_disable_when_required() -> None:
|
||||
"""An explicit 'enable_ipv6: false' conflicting with require_ipv6() is rejected."""
|
||||
require_ipv6("test")({})
|
||||
with pytest.raises(Invalid, match="explicitly disables it"):
|
||||
_final_validate({CONF_ENABLE_IPV6: False})
|
||||
|
||||
|
||||
def test_final_validate_accepts_explicit_ipv6_disable_when_not_required() -> None:
|
||||
"""An explicit 'enable_ipv6: false' is accepted when nothing requires IPv6."""
|
||||
_final_validate({CONF_ENABLE_IPV6: False})
|
||||
|
||||
|
||||
def test_final_validate_resolves_ipv6_default_when_required(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""require_ipv6() turns an absent 'enable_ipv6' on."""
|
||||
set_core_config(
|
||||
PlatformFramework.HOST_NATIVE,
|
||||
core_data={KEY_FRAMEWORK_VERSION: Version(0, 0, 0)},
|
||||
)
|
||||
require_ipv6("test")({})
|
||||
config: dict[str, bool] = {}
|
||||
_final_validate(config)
|
||||
assert config[CONF_ENABLE_IPV6] is True
|
||||
|
||||
|
||||
def test_final_validate_rejects_explicit_ipv4_disable_when_required() -> None:
|
||||
"""An explicit 'enable_ipv4: false' conflicting with require_ipv4() is rejected."""
|
||||
require_ipv4("test")({})
|
||||
with pytest.raises(Invalid, match="explicitly disables it"):
|
||||
_final_validate({CONF_ENABLE_IPV4: False})
|
||||
|
||||
|
||||
def test_final_validate_accepts_explicit_ipv4_disable_when_not_required() -> None:
|
||||
"""An explicit 'enable_ipv4: false' is accepted when IPv6 is on."""
|
||||
_final_validate({CONF_ENABLE_IPV4: False, CONF_ENABLE_IPV6: True})
|
||||
|
||||
|
||||
def test_final_validate_accepts_ipv4_default_when_required() -> None:
|
||||
"""An explicit 'enable_ipv4: true' never conflicts with require_ipv4()."""
|
||||
require_ipv4("test")({})
|
||||
_final_validate({CONF_ENABLE_IPV4: True})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[{CONF_ENABLE_IPV4: False}, {CONF_ENABLE_IPV4: False, CONF_ENABLE_IPV6: False}],
|
||||
)
|
||||
def test_final_validate_rejects_both_address_families_disabled(
|
||||
config: dict[str, bool],
|
||||
) -> None:
|
||||
"""Disabling IPv4 with IPv6 off leaves no IP stack and is rejected."""
|
||||
with pytest.raises(Invalid, match="leaves no IP stack"):
|
||||
_final_validate(config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config_file",
|
||||
[
|
||||
"esp8266_no_network.yaml",
|
||||
"rp2_no_network.yaml",
|
||||
"bk72xx_no_network.yaml",
|
||||
"esp32_idf_no_network.yaml",
|
||||
"esp32_arduino_no_network.yaml",
|
||||
"nrf52_no_network.yaml",
|
||||
],
|
||||
)
|
||||
def test_no_ip_defines_without_network_component(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
) -> None:
|
||||
"""Platform IP requests emit no IP family defines when network is not loaded."""
|
||||
generate_main(component_config_path(config_file))
|
||||
assert _define_value("USE_NETWORK_IPV4") is None
|
||||
assert _define_value("USE_NETWORK_IPV6") is None
|
||||
@@ -182,10 +182,6 @@ def test_border_router_rcp_codegen(
|
||||
lambda config, _: config[CONF_WIFI].update({CONF_AP: {}}),
|
||||
"does not support Wi-Fi AP",
|
||||
),
|
||||
(
|
||||
lambda config, _: config[CONF_NETWORK].update({CONF_ENABLE_IPV6: False}),
|
||||
"requires IPv6 to be enabled",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_border_router_rejects_incompatible_config(
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_constant_packets_share_progmem_tables(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Equal constant packets share one table; lambdas stay templates."""
|
||||
"""Equal constant packets share one table; a lambda is sent from its vector."""
|
||||
main_cpp = generate_main(component_config_path("packet_tables.yaml"))
|
||||
|
||||
tables = dict(
|
||||
@@ -26,8 +26,10 @@ def test_constant_packets_share_progmem_tables(
|
||||
["{0xC5, 0x51, 0x78, 0x82}", "{0x68, 0x69}"]
|
||||
)
|
||||
shared = next(k for k, v in tables.items() if v == "{0xC5, 0x51, 0x78, 0x82}")
|
||||
assert main_cpp.count(f"set_data_static({shared}, 4);") == 2
|
||||
assert "set_data_template(" in main_cpp
|
||||
assert main_cpp.count(f"->transmit_packet(::{shared}, 4);") == 2
|
||||
assert (
|
||||
"->transmit_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
)
|
||||
assert "set_sx1261" not in main_cpp
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_constant_packets_share_progmem_tables(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Equal constant packets share one table; lambdas stay templates."""
|
||||
"""Equal constant packets share one table; a lambda is sent from its vector."""
|
||||
main_cpp = generate_main(component_config_path("packet_tables.yaml"))
|
||||
|
||||
tables = dict(
|
||||
@@ -26,8 +26,10 @@ def test_constant_packets_share_progmem_tables(
|
||||
["{0xC5, 0x51, 0x78, 0x82}", "{0x68, 0x69}"]
|
||||
)
|
||||
shared = next(k for k, v in tables.items() if v == "{0xC5, 0x51, 0x78, 0x82}")
|
||||
assert main_cpp.count(f"set_data_static({shared}, 4);") == 2
|
||||
assert "set_data_template(" in main_cpp
|
||||
assert main_cpp.count(f"->transmit_packet(::{shared}, 4);") == 2
|
||||
assert (
|
||||
"->transmit_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_packet_data_length_limit() -> None:
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
on_boot:
|
||||
then:
|
||||
- uart.write: [0x01, 0x02, 0x03]
|
||||
- uart.write: !lambda return {0x09};
|
||||
- uart.write: ""
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
uart:
|
||||
tx_pin: GPIO1
|
||||
rx_pin: GPIO3
|
||||
baud_rate: 9600
|
||||
|
||||
switch:
|
||||
- platform: uart
|
||||
id: shared_switch
|
||||
name: Shared Switch
|
||||
data: [0x01, 0x02, 0x03]
|
||||
- platform: uart
|
||||
id: on_off_switch
|
||||
name: On Off Switch
|
||||
data:
|
||||
turn_on: "ON"
|
||||
turn_off: [0x04]
|
||||
|
||||
button:
|
||||
- platform: uart
|
||||
id: shared_button
|
||||
name: Shared Button
|
||||
data: [0x01, 0x02, 0x03]
|
||||
@@ -23,6 +23,19 @@ def test_constant_payloads_share_progmem_tables(
|
||||
)
|
||||
assert f"shared_switch->set_data_on({shared}, 3);" in main_cpp
|
||||
assert f"shared_button->set_data({shared}, 3);" in main_cpp
|
||||
assert f"set_data_static({shared}, 3);" in main_cpp
|
||||
assert "set_data_static(nullptr, 0);" in main_cpp
|
||||
assert "set_data_template(" in main_cpp
|
||||
assert f"->write_array(::{shared}, 3);" in main_cpp
|
||||
assert "->write_array(nullptr, 0);" in main_cpp
|
||||
assert "->write_array(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
|
||||
|
||||
def test_esp8266_write_action_reads_flash_directly(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""On ESP8266 the write action hands the table to write_array_progmem instead of copying it."""
|
||||
main_cpp = generate_main(component_config_path("payload_tables_esp8266.yaml"))
|
||||
|
||||
assert "->write_array_progmem(::uart_data, 3);" in main_cpp
|
||||
assert "->write_array_progmem(nullptr, 0);" in main_cpp
|
||||
assert "->write_array(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
assert "progmem_memcpy" not in main_cpp
|
||||
|
||||
@@ -4,12 +4,14 @@ from ipaddress import ip_address
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import network
|
||||
from esphome.components.udp import (
|
||||
CONF_ADDRESSES,
|
||||
CONF_LISTEN_ADDRESS,
|
||||
_address,
|
||||
_final_validate,
|
||||
_listen_address,
|
||||
_require_ipv6_for_ipv6_addresses,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ENABLE_IPV6, PlatformFramework
|
||||
@@ -61,29 +63,12 @@ _RAW_LWIP_IPV6 = {
|
||||
"network": {CONF_ENABLE_IPV6: True},
|
||||
"socket": {"implementation": "lwip_tcp"},
|
||||
}
|
||||
_NO_IPV6 = "requires 'enable_ipv6: true'"
|
||||
_RAW_LWIP = "not supported on this platform"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("addresses", "listen_address", "platform", "full_config", "message"),
|
||||
[
|
||||
pytest.param(
|
||||
["ff12::cafe"],
|
||||
"255.255.255.255",
|
||||
PlatformFramework.ESP32_IDF,
|
||||
{},
|
||||
_NO_IPV6,
|
||||
id="ipv6_destination_without_ipv6",
|
||||
),
|
||||
pytest.param(
|
||||
["255.255.255.255"],
|
||||
"ff12::cafe",
|
||||
PlatformFramework.ESP32_IDF,
|
||||
{},
|
||||
_NO_IPV6,
|
||||
id="ipv6_listen_without_ipv6",
|
||||
),
|
||||
pytest.param(
|
||||
["ff12::cafe"],
|
||||
"255.255.255.255",
|
||||
@@ -120,6 +105,31 @@ def test_final_validate_rejects_unsupported_ipv6(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("addresses", "listen_address", "required"),
|
||||
[
|
||||
(["ff12::cafe"], "255.255.255.255", True),
|
||||
(["255.255.255.255"], "ff12::cafe", True),
|
||||
(["255.255.255.255"], "255.255.255.255", False),
|
||||
],
|
||||
)
|
||||
def test_ipv6_address_requires_ipv6_from_network(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
addresses: list[str],
|
||||
listen_address: str,
|
||||
required: bool,
|
||||
) -> None:
|
||||
"""An IPv6 destination or listen address registers udp with network.require_ipv6()."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
_require_ipv6_for_ipv6_addresses(
|
||||
{
|
||||
CONF_ADDRESSES: [ip_address(a) for a in addresses],
|
||||
CONF_LISTEN_ADDRESS: ip_address(listen_address),
|
||||
}
|
||||
)
|
||||
assert ("udp" in network._get_data().require_ipv6) is required
|
||||
|
||||
|
||||
def test_final_validate_accepts_ipv6_when_enabled(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
|
||||
@@ -8,10 +8,17 @@ def test_write_payloads_share_a_progmem_table(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Identical constant payloads share one flash table; an empty one needs none."""
|
||||
"""Identical constant payloads share one flash table and are copied out of it on
|
||||
ESP8266 before sending; an empty one needs none; a lambda is sent from its vector."""
|
||||
main_cpp = generate_main(component_config_path("udp_write.yaml"))
|
||||
|
||||
assert main_cpp.count("udp_data[] PROGMEM = {0xAA, 0x55, 0x01, 0x02};") == 1
|
||||
assert main_cpp.count("->set_data_static(udp_data, 4);") == 2
|
||||
assert "->set_data_static(nullptr, 0);" in main_cpp
|
||||
assert "->set_data_template(" in main_cpp
|
||||
table = "esphome__udp_write__bytes"
|
||||
assert main_cpp.count(f"{table}[] PROGMEM = {{0xAA, 0x55, 0x01, 0x02}};") == 1
|
||||
assert (
|
||||
main_cpp.count(f"esphome::progmem_memcpy(esphome__bytes, ::{table}, 4);") == 2
|
||||
)
|
||||
assert main_cpp.count("->send_packet(esphome__bytes, 4);") == 2
|
||||
assert "uint8_t esphome__bytes[1];" in main_cpp
|
||||
assert "->send_packet(esphome__bytes, 0);" in main_cpp
|
||||
assert "->send_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
assert main_cpp.count("->set_should_broadcast();") == 4
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
|
||||
# OpenThread turns IPv4 off, so the dial path builds without any IPv4 code
|
||||
openthread:
|
||||
tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8
|
||||
|
||||
api:
|
||||
outgoing_connection:
|
||||
host: fd53:145f:ed22:ad81::1
|
||||
port: 6054
|
||||
delay: 60s
|
||||
@@ -1,30 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
static_assert([] {
|
||||
uint8_t v = 0;
|
||||
assign_bits(v, 1 << 3, true);
|
||||
return v;
|
||||
}() == 0x08);
|
||||
|
||||
TEST(AssignBits, SetsAndClearsOnlyTheMask) {
|
||||
uint8_t reg = 0b1010'0001;
|
||||
assign_bits(reg, 0b0000'0110, true);
|
||||
EXPECT_EQ(reg, 0b1010'0111);
|
||||
assign_bits(reg, 0b1000'0010, false);
|
||||
EXPECT_EQ(reg, 0b0010'0101);
|
||||
}
|
||||
|
||||
TEST(AssignBits, ClearsAtTheTargetWidth) {
|
||||
uint16_t reg16 = 0xFFFF;
|
||||
assign_bits(reg16, 1 << 15, false);
|
||||
EXPECT_EQ(reg16, 0x7FFF);
|
||||
uint64_t reg64 = UINT64_MAX;
|
||||
assign_bits(reg64, uint32_t{1} << 31, false);
|
||||
EXPECT_EQ(reg64, UINT64_MAX & ~(uint64_t{1} << 31));
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -0,0 +1,5 @@
|
||||
substitutions:
|
||||
network_enable_ipv6: "true"
|
||||
|
||||
packages:
|
||||
network: !include common.yaml
|
||||
@@ -0,0 +1,12 @@
|
||||
esp32:
|
||||
framework:
|
||||
log_level: DEBUG
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
enable_ipv4: false
|
||||
|
||||
api:
|
||||
|
||||
openthread:
|
||||
tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8
|
||||
@@ -1,5 +1,6 @@
|
||||
network:
|
||||
enable_ipv6: true
|
||||
enable_ipv4: false
|
||||
|
||||
openthread:
|
||||
tlv: 0E080000000000010000
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
network:
|
||||
enable_ipv6: true
|
||||
enable_ipv4: true
|
||||
|
||||
openthread:
|
||||
tlv: 0E080000000000010000
|
||||
|
||||
@@ -45,7 +45,7 @@ The `InitialStateHelper` class solves a common problem in integration tests: whe
|
||||
- Tracks all entities (except stateless ones like buttons)
|
||||
- Swallows the first state broadcast for each entity
|
||||
- Forwards all subsequent state changes to your test callback
|
||||
- Provides `wait_for_initial_states()` to synchronize before test actions
|
||||
- Provides `wait_for_initial_states()` to synchronize before test actions; it fails the test on timeout, naming the entities still without a state
|
||||
|
||||
**When to use it:**
|
||||
- Any test that triggers entity state changes and needs to verify them
|
||||
@@ -232,11 +232,9 @@ initial_state_helper = InitialStateHelper(entities)
|
||||
# Subscribe with the wrapper that filters initial states
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for all initial states to be broadcast
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
# Wait for all initial states to be broadcast; on timeout this fails the test
|
||||
# and lists the entities that never sent one
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
# Now perform your test actions - on_state will only receive new changes
|
||||
# ... trigger state changes ...
|
||||
|
||||
@@ -139,7 +139,7 @@ button:
|
||||
on_press:
|
||||
- lambda: |-
|
||||
// Start polling now that the test has subscribed.
|
||||
id(modbus_controller_1).set_update_interval(1000);
|
||||
id(modbus_controller_1).set_update_interval(300);
|
||||
id(modbus_controller_1).start_poller();
|
||||
// Broadcast (address 0) write single register: reg 0x10 = 777 on every server.
|
||||
// PDU is function code + data (no address/CRC); the hub prepends address 0 and appends CRC.
|
||||
|
||||
@@ -46,7 +46,7 @@ modbus:
|
||||
role: client
|
||||
turnaround_time: 10ms
|
||||
# Short wait so the no-reply cases (address 2 below) time out well within the test window.
|
||||
send_wait_time: 500ms
|
||||
send_wait_time: 200ms
|
||||
|
||||
modbus_server:
|
||||
- address: 1
|
||||
|
||||
@@ -75,7 +75,7 @@ modbus_controller:
|
||||
- address: 1
|
||||
modbus_id: virtual_modbus_controller
|
||||
id: modbus_controller_1
|
||||
update_interval: 1s
|
||||
update_interval: 500ms
|
||||
|
||||
modbus_server:
|
||||
- address: 1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user