1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-09 17:21:57 +00:00

Compare commits

..

46 Commits

Author SHA1 Message Date
J. Nick Koston
ce5b93d9ca Merge branch 'dev' into compact_string_wifi 2026-02-09 07:24:27 -06:00
J. Nick Koston
22c77866d8 [e131] Remove unnecessary heap allocation from packet receive loop (#13852) 2026-02-09 06:42:26 -06:00
J. Nick Koston
790ac620ab [web_server_idf] Use C++17 nested namespace style (#13856) 2026-02-09 06:42:12 -06:00
tronikos
fb93283720 [water_heater] Add state masking to distinguish explicit commands from no-change (#13879) 2026-02-09 03:52:49 -06:00
J. Nick Koston
bed01da345 [api] Guard varint parsing against overlong encodings (#13870) 2026-02-09 03:45:40 -06:00
J. Nick Koston
422f413680 [lps22] Replace set_retry with set_interval to avoid heap allocation (#13841) 2026-02-09 03:26:44 -06:00
J. Nick Koston
c3c0c40524 [mqtt] Return friendly_name_() by const reference to avoid string copies (#13810) 2026-02-09 03:26:29 -06:00
J. Nick Koston
46f8302d8f [mqtt] Use stack buffer for discovery topic to avoid heap allocation (#13812) 2026-02-09 03:26:15 -06:00
J. Nick Koston
e24528c842 [analyze-memory] Attribute CSWTCH symbols from SDK archives (#13850) 2026-02-09 03:25:59 -06:00
J. Nick Koston
5370687001 [wizard] Use secrets module for fallback AP password generation (#13864) 2026-02-09 03:25:41 -06:00
J. Nick Koston
6ee185c58a [dashboard] Use resolve/relative_to for download path validation (#13867) 2026-02-09 03:25:23 -06:00
J. Nick Koston
eb6a6f8d0d [web_server_idf] Remove unused host() method (#13869) 2026-02-09 03:25:05 -06:00
J. Nick Koston
140ec0639c [api] Elide empty message construction in protobuf dispatch (#13871) 2026-02-09 03:24:45 -06:00
Clyde Stubbs
756f1c6b7e [lvgl] Fix crash with unconfigured top_layer (#13846) 2026-02-08 21:53:43 -05:00
tomaszduda23
28b9487b25 [nrf52,logger] fix printk (#13874) 2026-02-08 17:52:05 +00:00
J. Nick Koston
41fedaedb3 [udp] Eliminate per-loop heap allocation using std::span (#13838)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-02-08 08:26:47 -06:00
J. Nick Koston
6f9eee42d6 Merge branch 'dev' into compact_string_wifi 2026-02-03 02:20:49 +01:00
J. Nick Koston
a3c2248b44 Merge upstream/dev into compact_string_wifi 2026-01-28 05:51:28 -10:00
J. Nick Koston
d75254309f Merge branch 'filter_wifi_scan_results' into compact_string_wifi 2026-01-25 20:08:49 -10:00
J. Nick Koston
5d1acb0cb8 Merge branch 'dev' into filter_wifi_scan_results 2026-01-25 20:08:41 -10:00
J. Nick Koston
cafc7651c2 Merge branch 'filter_wifi_scan_results' into compact_string_wifi 2026-01-25 17:24:41 -10:00
J. Nick Koston
4099e944d6 tweak 2026-01-25 17:22:00 -10:00
J. Nick Koston
5ad989a13a Merge remote-tracking branch 'upstream/dev' into filter_wifi_scan_results
# Conflicts:
#	esphome/components/wifi/wifi_component_esp_idf.cpp
2026-01-25 17:17:27 -10:00
J. Nick Koston
7336985753 reduce some more 2026-01-22 17:53:50 -10:00
J. Nick Koston
73d076c278 reduce some more 2026-01-22 17:35:00 -10:00
J. Nick Koston
3a2c66171b use placement new to avoid duplicate code 2026-01-22 17:29:21 -10:00
J. Nick Koston
fca867e18d [wifi] Add CompactString to reduce WiFi scan heap fragmentation 2026-01-22 17:18:13 -10:00
J. Nick Koston
0ae90512cf [wifi] Add CompactString to reduce WiFi scan heap fragmentation 2026-01-22 17:16:35 -10:00
J. Nick Koston
165f81dc97 Merge branch 'dev' into filter_wifi_scan_results 2026-01-22 15:05:38 -10:00
J. Nick Koston
dc971b4ed0 tidy 2026-01-20 22:54:52 -10:00
J. Nick Koston
a4fe9852aa tidy 2026-01-20 22:54:36 -10:00
J. Nick Koston
f6ec5e9c28 tweak 2026-01-20 22:41:45 -10:00
J. Nick Koston
0051196e86 fix 2026-01-20 21:41:43 -10:00
J. Nick Koston
9f83b24913 tweak 2026-01-20 21:19:30 -10:00
J. Nick Koston
5c0747cfe0 Update esphome/components/wifi/wifi_component.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-20 21:10:51 -10:00
J. Nick Koston
f0c7306ad5 log scan complete 2026-01-20 21:04:52 -10:00
J. Nick Koston
09b42b778b log scan complete 2026-01-20 20:57:39 -10:00
J. Nick Koston
d610c3ae91 fix bssid only 2026-01-20 20:54:30 -10:00
J. Nick Koston
687f9a762d fixes for libretiny 2026-01-20 20:44:28 -10:00
J. Nick Koston
acb22ed286 tweaks 2026-01-20 20:39:30 -10:00
J. Nick Koston
692167341e tweaks 2026-01-20 20:37:16 -10:00
J. Nick Koston
d5d6936845 tweaks 2026-01-20 20:35:32 -10:00
J. Nick Koston
bffe4a2e05 tweaks 2026-01-20 20:34:53 -10:00
J. Nick Koston
d7c3947ccc tweak loggig 2026-01-20 20:31:38 -10:00
J. Nick Koston
6f3a49e509 tweak loggig 2026-01-20 20:30:55 -10:00
J. Nick Koston
7aef173e65 [wifi] Filter scan results to only store matching networks 2026-01-20 20:19:35 -10:00
53 changed files with 670 additions and 404 deletions

View File

@@ -397,47 +397,38 @@ class MemoryAnalyzer:
return pioenvs_dir
return None
def _scan_cswtch_in_objects(
self, obj_dir: Path
) -> dict[str, list[tuple[str, int]]]:
"""Scan object files for CSWTCH symbols using a single nm invocation.
@staticmethod
def _parse_nm_cswtch_output(
output: str,
base_dir: Path | None,
cswtch_map: dict[str, list[tuple[str, int]]],
) -> None:
"""Parse nm output for CSWTCH symbols and add to cswtch_map.
Uses ``nm --print-file-name -S`` on all ``.o`` files at once.
Output format: ``/path/to/file.o:address size type name``
Handles both ``.o`` files and ``.a`` archives.
nm output formats::
.o files: /path/file.o:hex_addr hex_size type name
.a files: /path/lib.a:member.o:hex_addr hex_size type name
For ``.o`` files, paths are made relative to *base_dir* when possible.
For ``.a`` archives (detected by ``:`` in the file portion), paths are
formatted as ``archive_stem/member.o`` (e.g. ``liblwip2-536-feat/lwip-esp.o``).
Args:
obj_dir: Directory containing object files (.pioenvs/<env>/)
Returns:
Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples.
output: Raw stdout from ``nm --print-file-name -S``.
base_dir: Base directory for computing relative paths of ``.o`` files.
Pass ``None`` when scanning archives outside the build tree.
cswtch_map: Dict to populate, mapping ``"CSWTCH$N:size"`` to source list.
"""
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
if not self.nm_path:
return cswtch_map
# Find all .o files recursively, sorted for deterministic output
obj_files = sorted(obj_dir.rglob("*.o"))
if not obj_files:
return cswtch_map
_LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files))
# Single nm call with --print-file-name for all object files
result = run_tool(
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files],
timeout=30,
)
if result is None or result.returncode != 0:
return cswtch_map
for line in result.stdout.splitlines():
for line in output.splitlines():
if "CSWTCH$" not in line:
continue
# Split on last ":" that precedes a hex address.
# nm --print-file-name format: filepath:hex_addr hex_size type name
# We split from the right: find the last colon followed by hex digits.
# For .o: "filepath.o" : "hex_addr hex_size type name"
# For .a: "filepath.a:member.o" : "hex_addr hex_size type name"
parts_after_colon = line.rsplit(":", 1)
if len(parts_after_colon) != 2:
continue
@@ -457,16 +448,89 @@ class MemoryAnalyzer:
except ValueError:
continue
# Get relative path from obj_dir for readability
try:
rel_path = str(Path(file_path).relative_to(obj_dir))
except ValueError:
# Determine readable source path
# Use ".a:" to detect archive format (not bare ":" which matches
# Windows drive letters like "C:\...\file.o").
if ".a:" in file_path:
# Archive format: "archive.a:member.o" → "archive_stem/member.o"
archive_part, member = file_path.rsplit(":", 1)
archive_name = Path(archive_part).stem
rel_path = f"{archive_name}/{member}"
elif base_dir is not None:
try:
rel_path = str(Path(file_path).relative_to(base_dir))
except ValueError:
rel_path = file_path
else:
rel_path = file_path
key = f"{sym_name}:{size}"
cswtch_map[key].append((rel_path, size))
return cswtch_map
def _run_nm_cswtch_scan(
self,
files: list[Path],
base_dir: Path | None,
cswtch_map: dict[str, list[tuple[str, int]]],
) -> None:
"""Run nm on *files* and add any CSWTCH symbols to *cswtch_map*.
Args:
files: Object (``.o``) or archive (``.a``) files to scan.
base_dir: Base directory for relative path computation (see
:meth:`_parse_nm_cswtch_output`).
cswtch_map: Dict to populate with results.
"""
if not self.nm_path or not files:
return
_LOGGER.debug("Scanning %d files for CSWTCH symbols", len(files))
result = run_tool(
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in files],
timeout=30,
)
if result is None or result.returncode != 0:
_LOGGER.debug(
"nm failed or timed out scanning %d files for CSWTCH symbols",
len(files),
)
return
self._parse_nm_cswtch_output(result.stdout, base_dir, cswtch_map)
def _scan_cswtch_in_sdk_archives(
self, cswtch_map: dict[str, list[tuple[str, int]]]
) -> None:
"""Scan SDK library archives (.a) for CSWTCH symbols.
Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source,
so their CSWTCH symbols only exist inside ``.a`` archives. Results are
merged into *cswtch_map* for keys not already found in ``.o`` files.
The same source file (e.g. ``lwip-esp.o``) often appears in multiple
library variants (``liblwip2-536.a``, ``liblwip2-1460-feat.a``, etc.),
so results are deduplicated by member name.
"""
sdk_dirs = self._find_sdk_library_dirs()
if not sdk_dirs:
return
sdk_archives = sorted(a for sdk_dir in sdk_dirs for a in sdk_dir.glob("*.a"))
sdk_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
self._run_nm_cswtch_scan(sdk_archives, None, sdk_map)
# Merge SDK results, deduplicating by member name.
for key, sources in sdk_map.items():
if key in cswtch_map:
continue
seen: dict[str, tuple[str, int]] = {}
for path, sz in sources:
member = Path(path).name
if member not in seen:
seen[member] = (path, sz)
cswtch_map[key] = list(seen.values())
def _source_file_to_component(self, source_file: str) -> str:
"""Map a source object file path to its component name.
@@ -505,17 +569,25 @@ class MemoryAnalyzer:
CSWTCH symbols are compiler-generated lookup tables for switch statements.
They are local symbols, so the same name can appear in different object files.
This method scans .o files to attribute them to their source components.
This method scans .o files and SDK archives to attribute them to their
source components.
"""
obj_dir = self._find_object_files_dir()
if obj_dir is None:
_LOGGER.debug("No object files directory found, skipping CSWTCH analysis")
return
# Scan object files for CSWTCH symbols
cswtch_map = self._scan_cswtch_in_objects(obj_dir)
# Scan build-dir object files for CSWTCH symbols
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
self._run_nm_cswtch_scan(sorted(obj_dir.rglob("*.o")), obj_dir, cswtch_map)
# Also scan SDK library archives (.a) for CSWTCH symbols.
# Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source
# so their symbols only exist inside .a archives, not as loose .o files.
self._scan_cswtch_in_sdk_archives(cswtch_map)
if not cswtch_map:
_LOGGER.debug("No CSWTCH symbols found in object files")
_LOGGER.debug("No CSWTCH symbols found in object files or SDK archives")
return
# Collect CSWTCH symbols from the ELF (already parsed in sections)

View File

@@ -87,6 +87,7 @@ from esphome.cpp_types import ( # noqa: F401
size_t,
std_ns,
std_shared_ptr,
std_span,
std_string,
std_string_ref,
std_vector,

View File

@@ -283,7 +283,7 @@ void APIConnection::loop() {
#endif
}
bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
bool APIConnection::send_disconnect_response() {
// remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response
// close will happen on next loop
@@ -292,7 +292,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
DisconnectResponse resp;
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
}
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
void APIConnection::on_disconnect_response() {
this->helper_->close();
this->flags_.remove = true;
}
@@ -1095,7 +1095,7 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
}
void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
void APIConnection::unsubscribe_bluetooth_le_advertisements() {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
}
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) {
@@ -1121,8 +1121,7 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg)
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg);
}
bool APIConnection::send_subscribe_bluetooth_connections_free_response(
const SubscribeBluetoothConnectionsFreeRequest &msg) {
bool APIConnection::send_subscribe_bluetooth_connections_free_response() {
bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this);
return true;
}
@@ -1491,12 +1490,12 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
}
bool APIConnection::send_ping_response(const PingRequest &msg) {
bool APIConnection::send_ping_response() {
PingResponse resp;
return this->send_message(resp, PingResponse::MESSAGE_TYPE);
}
bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
bool APIConnection::send_device_info_response() {
DeviceInfoResponse resp{};
resp.name = StringRef(App.get_name());
resp.friendly_name = StringRef(App.get_friendly_name());
@@ -1746,9 +1745,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
state_subs_at_ = 0;
}
void APIConnection::subscribe_home_assistant_states() { state_subs_at_ = 0; }
#endif
bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
if (this->flags_.remove)

View File

@@ -127,7 +127,7 @@ class APIConnection final : public APIServerConnection {
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements() override;
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
@@ -136,7 +136,7 @@ class APIConnection final : public APIServerConnection {
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
bool send_subscribe_bluetooth_connections_free_response() override;
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
#endif
@@ -187,8 +187,8 @@ class APIConnection final : public APIServerConnection {
void update_command(const UpdateCommandRequest &msg) override;
#endif
void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override {
void on_disconnect_response() override;
void on_ping_response() override {
// we initiated ping
this->flags_.sent_ping = false;
}
@@ -199,11 +199,11 @@ class APIConnection final : public APIServerConnection {
void on_get_time_response(const GetTimeResponse &value) override;
#endif
bool send_hello_response(const HelloRequest &msg) override;
bool send_disconnect_response(const DisconnectRequest &msg) override;
bool send_ping_response(const PingRequest &msg) override;
bool send_device_info_response(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void subscribe_states(const SubscribeStatesRequest &msg) override {
bool send_disconnect_response() override;
bool send_ping_response() override;
bool send_device_info_response() override;
void list_entities() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void subscribe_states() override {
this->flags_.state_subscription = true;
// Start initial state iterator only if no iterator is active
// If list_entities is running, we'll start initial_state when it completes
@@ -217,12 +217,10 @@ class APIConnection final : public APIServerConnection {
App.schedule_dump_config();
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
this->flags_.service_call_subscription = true;
}
void subscribe_homeassistant_services() override { this->flags_.service_call_subscription = true; }
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
void subscribe_home_assistant_states() override;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
void execute_service(const ExecuteServiceRequest &msg) override;

View File

@@ -15,6 +15,9 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name, const
DumpBuffer dump_buf;
ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf));
}
void APIServerConnectionBase::log_receive_message_(const LogString *name) {
ESP_LOGVV(TAG, "%s: {}", LOG_STR_ARG(name));
}
#endif
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
@@ -29,66 +32,52 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break;
}
case DisconnectRequest::MESSAGE_TYPE: {
DisconnectRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_disconnect_request"), msg);
this->log_receive_message_(LOG_STR("on_disconnect_request"));
#endif
this->on_disconnect_request(msg);
this->on_disconnect_request();
break;
}
case DisconnectResponse::MESSAGE_TYPE: {
DisconnectResponse msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_disconnect_response"), msg);
this->log_receive_message_(LOG_STR("on_disconnect_response"));
#endif
this->on_disconnect_response(msg);
this->on_disconnect_response();
break;
}
case PingRequest::MESSAGE_TYPE: {
PingRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_ping_request"), msg);
this->log_receive_message_(LOG_STR("on_ping_request"));
#endif
this->on_ping_request(msg);
this->on_ping_request();
break;
}
case PingResponse::MESSAGE_TYPE: {
PingResponse msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_ping_response"), msg);
this->log_receive_message_(LOG_STR("on_ping_response"));
#endif
this->on_ping_response(msg);
this->on_ping_response();
break;
}
case DeviceInfoRequest::MESSAGE_TYPE: {
DeviceInfoRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_device_info_request"), msg);
this->log_receive_message_(LOG_STR("on_device_info_request"));
#endif
this->on_device_info_request(msg);
this->on_device_info_request();
break;
}
case ListEntitiesRequest::MESSAGE_TYPE: {
ListEntitiesRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_list_entities_request"), msg);
this->log_receive_message_(LOG_STR("on_list_entities_request"));
#endif
this->on_list_entities_request(msg);
this->on_list_entities_request();
break;
}
case SubscribeStatesRequest::MESSAGE_TYPE: {
SubscribeStatesRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_states_request"), msg);
this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
#endif
this->on_subscribe_states_request(msg);
this->on_subscribe_states_request();
break;
}
case SubscribeLogsRequest::MESSAGE_TYPE: {
@@ -146,12 +135,10 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
SubscribeHomeassistantServicesRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"), msg);
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
#endif
this->on_subscribe_homeassistant_services_request(msg);
this->on_subscribe_homeassistant_services_request();
break;
}
#endif
@@ -166,12 +153,10 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
#ifdef USE_API_HOMEASSISTANT_STATES
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
SubscribeHomeAssistantStatesRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"), msg);
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
#endif
this->on_subscribe_home_assistant_states_request(msg);
this->on_subscribe_home_assistant_states_request();
break;
}
#endif
@@ -375,23 +360,19 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#endif
#ifdef USE_BLUETOOTH_PROXY
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
SubscribeBluetoothConnectionsFreeRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"), msg);
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
#endif
this->on_subscribe_bluetooth_connections_free_request(msg);
this->on_subscribe_bluetooth_connections_free_request();
break;
}
#endif
#ifdef USE_BLUETOOTH_PROXY
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
UnsubscribeBluetoothLEAdvertisementsRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"), msg);
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
#endif
this->on_unsubscribe_bluetooth_le_advertisements_request(msg);
this->on_unsubscribe_bluetooth_le_advertisements_request();
break;
}
#endif
@@ -647,36 +628,29 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) {
this->on_fatal_error();
}
}
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
if (!this->send_disconnect_response(msg)) {
void APIServerConnection::on_disconnect_request() {
if (!this->send_disconnect_response()) {
this->on_fatal_error();
}
}
void APIServerConnection::on_ping_request(const PingRequest &msg) {
if (!this->send_ping_response(msg)) {
void APIServerConnection::on_ping_request() {
if (!this->send_ping_response()) {
this->on_fatal_error();
}
}
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
if (!this->send_device_info_response(msg)) {
void APIServerConnection::on_device_info_request() {
if (!this->send_device_info_response()) {
this->on_fatal_error();
}
}
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { this->list_entities(msg); }
void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
this->subscribe_states(msg);
}
void APIServerConnection::on_list_entities_request() { this->list_entities(); }
void APIServerConnection::on_subscribe_states_request() { this->subscribe_states(); }
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); }
#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServerConnection::on_subscribe_homeassistant_services_request(
const SubscribeHomeassistantServicesRequest &msg) {
this->subscribe_homeassistant_services(msg);
}
void APIServerConnection::on_subscribe_homeassistant_services_request() { this->subscribe_homeassistant_services(); }
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
this->subscribe_home_assistant_states(msg);
}
void APIServerConnection::on_subscribe_home_assistant_states_request() { this->subscribe_home_assistant_states(); }
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
@@ -793,17 +767,15 @@ void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNo
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
const SubscribeBluetoothConnectionsFreeRequest &msg) {
if (!this->send_subscribe_bluetooth_connections_free_response(msg)) {
void APIServerConnection::on_subscribe_bluetooth_connections_free_request() {
if (!this->send_subscribe_bluetooth_connections_free_response()) {
this->on_fatal_error();
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
this->unsubscribe_bluetooth_le_advertisements(msg);
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request() {
this->unsubscribe_bluetooth_le_advertisements();
}
#endif
#ifdef USE_BLUETOOTH_PROXY

View File

@@ -14,6 +14,7 @@ class APIServerConnectionBase : public ProtoService {
protected:
void log_send_message_(const char *name, const char *dump);
void log_receive_message_(const LogString *name, const ProtoMessage &msg);
void log_receive_message_(const LogString *name);
public:
#endif
@@ -28,15 +29,15 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_hello_request(const HelloRequest &value){};
virtual void on_disconnect_request(const DisconnectRequest &value){};
virtual void on_disconnect_response(const DisconnectResponse &value){};
virtual void on_ping_request(const PingRequest &value){};
virtual void on_ping_response(const PingResponse &value){};
virtual void on_device_info_request(const DeviceInfoRequest &value){};
virtual void on_disconnect_request(){};
virtual void on_disconnect_response(){};
virtual void on_ping_request(){};
virtual void on_ping_response(){};
virtual void on_device_info_request(){};
virtual void on_list_entities_request(const ListEntitiesRequest &value){};
virtual void on_list_entities_request(){};
virtual void on_subscribe_states_request(const SubscribeStatesRequest &value){};
virtual void on_subscribe_states_request(){};
#ifdef USE_COVER
virtual void on_cover_command_request(const CoverCommandRequest &value){};
@@ -61,14 +62,14 @@ class APIServerConnectionBase : public ProtoService {
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
virtual void on_subscribe_homeassistant_services_request(){};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
virtual void on_subscribe_home_assistant_states_request(){};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
@@ -147,12 +148,11 @@ class APIServerConnectionBase : public ProtoService {
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &value){};
virtual void on_subscribe_bluetooth_connections_free_request(){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_unsubscribe_bluetooth_le_advertisements_request(
const UnsubscribeBluetoothLEAdvertisementsRequest &value){};
virtual void on_unsubscribe_bluetooth_le_advertisements_request(){};
#endif
#ifdef USE_BLUETOOTH_PROXY
@@ -231,17 +231,17 @@ class APIServerConnectionBase : public ProtoService {
class APIServerConnection : public APIServerConnectionBase {
public:
virtual bool send_hello_response(const HelloRequest &msg) = 0;
virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
virtual bool send_ping_response(const PingRequest &msg) = 0;
virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
virtual void list_entities(const ListEntitiesRequest &msg) = 0;
virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
virtual bool send_disconnect_response() = 0;
virtual bool send_ping_response() = 0;
virtual bool send_device_info_response() = 0;
virtual void list_entities() = 0;
virtual void subscribe_states() = 0;
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
#ifdef USE_API_HOMEASSISTANT_SERVICES
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
virtual void subscribe_homeassistant_services() = 0;
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
virtual void subscribe_home_assistant_states() = 0;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
@@ -331,11 +331,10 @@ class APIServerConnection : public APIServerConnectionBase {
virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual bool send_subscribe_bluetooth_connections_free_response(
const SubscribeBluetoothConnectionsFreeRequest &msg) = 0;
virtual bool send_subscribe_bluetooth_connections_free_response() = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
virtual void unsubscribe_bluetooth_le_advertisements() = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0;
@@ -363,17 +362,17 @@ class APIServerConnection : public APIServerConnectionBase {
#endif
protected:
void on_hello_request(const HelloRequest &msg) override;
void on_disconnect_request(const DisconnectRequest &msg) override;
void on_ping_request(const PingRequest &msg) override;
void on_device_info_request(const DeviceInfoRequest &msg) override;
void on_list_entities_request(const ListEntitiesRequest &msg) override;
void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
void on_disconnect_request() override;
void on_ping_request() override;
void on_device_info_request() override;
void on_list_entities_request() override;
void on_subscribe_states_request() override;
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
void on_subscribe_homeassistant_services_request() override;
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
void on_subscribe_home_assistant_states_request() override;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
@@ -463,11 +462,10 @@ class APIServerConnection : public APIServerConnectionBase {
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
void on_subscribe_bluetooth_connections_free_request() override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_unsubscribe_bluetooth_le_advertisements_request(
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
void on_unsubscribe_bluetooth_le_advertisements_request() override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;

View File

@@ -112,8 +112,12 @@ class ProtoVarInt {
uint64_t result = buffer[0] & 0x7F;
uint8_t bitpos = 7;
// A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings
// to avoid undefined behavior from shifting uint64_t by >= 64 bits.
uint32_t max_len = std::min(len, uint32_t(10));
// Start from the second byte since we've already processed the first
for (uint32_t i = 1; i < len; i++) {
for (uint32_t i = 1; i < max_len; i++) {
uint8_t val = buffer[i];
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7;

View File

@@ -55,7 +55,6 @@ void E131Component::setup() {
}
void E131Component::loop() {
std::vector<uint8_t> payload;
E131Packet packet;
int universe = 0;
uint8_t buf[1460];
@@ -64,11 +63,9 @@ void E131Component::loop() {
if (len == -1) {
return;
}
payload.resize(len);
memmove(&payload[0], buf, len);
if (!this->packet_(payload, universe, packet)) {
ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
if (!this->packet_(buf, (size_t) len, universe, packet)) {
ESP_LOGV(TAG, "Invalid packet received of size %zd.", len);
return;
}

View File

@@ -38,7 +38,7 @@ class E131Component : public esphome::Component {
void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; }
protected:
bool packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet);
bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet);
bool process_(int universe, const E131Packet &packet);
bool join_igmp_groups_();
void join_(int universe);

View File

@@ -116,11 +116,11 @@ void E131Component::leave_(int universe) {
ESP_LOGD(TAG, "Left %d universe for E1.31.", universe);
}
bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet) {
if (data.size() < E131_MIN_PACKET_SIZE)
bool E131Component::packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet) {
if (len < E131_MIN_PACKET_SIZE)
return false;
auto *sbuff = reinterpret_cast<const E131RawPacket *>(&data[0]);
auto *sbuff = reinterpret_cast<const E131RawPacket *>(data);
if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0)
return false;

View File

@@ -267,16 +267,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
for (auto &scan : results) {
if (scan.get_is_hidden())
continue;
const std::string &ssid = scan.get_ssid();
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
const char *ssid_cstr = scan.get_ssid().c_str();
// Check if we've already sent this SSID
bool duplicate = false;
for (const auto &seen : networks) {
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
duplicate = true;
break;
}
}
if (duplicate)
continue;
// Only allocate std::string after confirming it's not a duplicate
std::string ssid(ssid_cstr);
// Send each ssid separately to avoid overflowing the buffer
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
std::vector<uint8_t> data =
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
this->send_response_(data);
networks.push_back(ssid);
networks.push_back(std::move(ssid));
}
// Send empty response to signify the end of the list.
std::vector<uint8_t> data =

View File

@@ -68,7 +68,7 @@ void HOT Logger::write_msg_(const char *msg, uint16_t len) {
#ifdef CONFIG_PRINTK
// Requires the debug component and an active SWD connection.
// It is used for pyocd rtt -t nrf52840
k_str_out(const_cast<char *>(msg), len);
printk("%.*s", static_cast<int>(len), msg);
#endif
if (this->uart_dev_ == nullptr) {
return;

View File

@@ -38,22 +38,29 @@ void LPS22Component::dump_config() {
LOG_UPDATE_INTERVAL(this);
}
static constexpr uint32_t INTERVAL_READ = 0;
void LPS22Component::update() {
uint8_t value = 0x00;
this->read_register(CTRL_REG2, &value, 1);
value |= CTRL_REG2_ONE_SHOT_MASK;
this->write_register(CTRL_REG2, &value, 1);
this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); });
this->read_attempts_remaining_ = READ_ATTEMPTS;
this->set_interval(INTERVAL_READ, READ_INTERVAL, [this]() { this->try_read_(); });
}
RetryResult LPS22Component::try_read_() {
void LPS22Component::try_read_() {
uint8_t value = 0x00;
this->read_register(STATUS, &value, 1);
const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK;
if ((value & expected_status_mask) != expected_status_mask) {
ESP_LOGD(TAG, "STATUS not ready: %x", value);
return RetryResult::RETRY;
if (--this->read_attempts_remaining_ == 0) {
this->cancel_interval(INTERVAL_READ);
}
return;
}
this->cancel_interval(INTERVAL_READ);
if (this->temperature_sensor_ != nullptr) {
uint8_t t_buf[2]{0};
@@ -68,7 +75,6 @@ RetryResult LPS22Component::try_read_() {
uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]);
this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast<float>(p_lsb));
}
return RetryResult::DONE;
}
} // namespace lps22

View File

@@ -17,10 +17,11 @@ class LPS22Component : public sensor::Sensor, public PollingComponent, public i2
void dump_config() override;
protected:
void try_read_();
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
RetryResult try_read_();
uint8_t read_attempts_remaining_{0};
};
} // namespace lps22

View File

@@ -436,6 +436,7 @@ def container_schema(widget_type: WidgetType, extras=None):
schema = schema.extend(widget_type.schema)
def validator(value):
value = value or {}
return append_layout_schema(schema, value)(value)
return validator

View File

@@ -34,10 +34,7 @@ inline char *append_char(char *p, char c) {
// MQTT_COMPONENT_TYPE_MAX_LEN, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are in mqtt_component.h.
// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h.
// This ensures the stack buffers below are always large enough.
static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 +
ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
// MQTT_DISCOVERY_PREFIX_MAX_LEN and MQTT_DISCOVERY_TOPIC_MAX_LEN are defined in mqtt_component.h
// Function implementation of LOG_MQTT_COMPONENT macro to reduce code size
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) {
@@ -54,15 +51,15 @@ void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos;
void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
StringRef MQTTComponent::get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
const MQTTDiscoveryInfo &discovery_info) const {
char sanitized_name[ESPHOME_DEVICE_NAME_MAX_LEN + 1];
str_sanitize_to(sanitized_name, App.get_name().c_str());
const char *comp_type = this->component_type();
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
char buf[DISCOVERY_TOPIC_MAX_LEN];
char *p = buf;
char *p = buf.data();
p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size());
p = append_char(p, '/');
@@ -72,8 +69,9 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size());
p = append_str(p, "/config", 7);
*p = '\0';
return std::string(buf, p - buf);
return StringRef(buf.data(), p - buf.data());
}
StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
@@ -182,16 +180,19 @@ bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f)
bool MQTTComponent::send_discovery_() {
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
char discovery_topic_buf[MQTT_DISCOVERY_TOPIC_MAX_LEN];
StringRef discovery_topic = this->get_discovery_topic_to_(discovery_topic_buf, discovery_info);
if (discovery_info.clean) {
ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str());
return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true);
return global_mqtt_client->publish(discovery_topic.c_str(), "", 0, this->qos_, true);
}
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str());
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return global_mqtt_client->publish_json(
this->get_discovery_topic_(discovery_info),
discovery_topic.c_str(),
[this](JsonObject root) {
SendDiscoveryConfig config;
config.state_topic = true;
@@ -204,7 +205,7 @@ bool MQTTComponent::send_discovery_() {
}
// Fields from EntityBase
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : "";
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : StringRef();
if (this->is_disabled_by_default_())
root[MQTT_ENABLED_BY_DEFAULT] = false;
@@ -248,7 +249,7 @@ bool MQTTComponent::send_discovery_() {
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
char friendly_name_hash[9];
buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32,
fnv1_hash(this->friendly_name_()));
fnv1_hash(this->friendly_name_().c_str()));
// Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
// MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11];
@@ -414,7 +415,7 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; }
bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); }
// Pull these properties from EntityBase if not overridden
std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); }
const StringRef &MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); }
StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const {
return this->get_entity()->get_object_id_to(buf);
}

View File

@@ -32,6 +32,10 @@ static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python:
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
static constexpr size_t MQTT_DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
static constexpr size_t MQTT_DISCOVERY_TOPIC_MAX_LEN = MQTT_DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN +
1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
class MQTTComponent; // Forward declaration
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
@@ -263,8 +267,9 @@ class MQTTComponent : public Component {
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0);
protected:
/// Helper method to get the discovery topic for this component.
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const;
/// Helper method to get the discovery topic for this component into a buffer.
StringRef get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
const MQTTDiscoveryInfo &discovery_info) const;
/** Get this components state/command/... topic into a buffer.
*
@@ -288,7 +293,7 @@ class MQTTComponent : public Component {
virtual const EntityBase *get_entity() const = 0;
/// Get the friendly name of this MQTT component.
std::string friendly_name_() const;
const StringRef &friendly_name_() const;
/// Get the icon field of this component as StringRef
StringRef get_icon_ref_() const;

View File

@@ -396,9 +396,9 @@ static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) {
/**
* Process a received packet
*/
void PacketTransport::process_(const std::vector<uint8_t> &data) {
void PacketTransport::process_(std::span<const uint8_t> data) {
auto ping_key_seen = !this->ping_pong_enable_;
PacketDecoder decoder((data.data()), data.size());
PacketDecoder decoder(data.data(), data.size());
char namebuf[256]{};
uint8_t byte;
FuData rdata{};

View File

@@ -9,8 +9,9 @@
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include <vector>
#include <map>
#include <span>
#include <vector>
/**
* Providing packet encoding functions for exchanging data with a remote host.
@@ -113,7 +114,7 @@ class PacketTransport : public PollingComponent {
virtual bool should_send() { return true; }
// to be called by child classes when a data packet is received.
void process_(const std::vector<uint8_t> &data);
void process_(std::span<const uint8_t> data);
void send_data_(bool all);
void flush_();
void add_data_(uint8_t key, const char *id, float data);

View File

@@ -13,7 +13,7 @@ from esphome.components.packet_transport import (
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
from esphome.core import ID
from esphome.cpp_generator import literal
from esphome.cpp_generator import MockObj
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["network"]
@@ -23,8 +23,12 @@ 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_args = cg.std_vector.template(cg.uint8)
trigger_argname = "data"
# Listener callback type (non-owning span from UDP component)
listener_args = cg.std_span.template(cg.uint8.operator("const"))
listener_argtype = [(listener_args, trigger_argname)]
# Automation/trigger type (owned vector, safe for deferred actions like delay)
trigger_args = cg.std_vector.template(cg.uint8)
trigger_argtype = [(trigger_args, trigger_argname)]
CONF_ADDRESSES = "addresses"
@@ -118,7 +122,13 @@ async def to_code(config):
trigger_id, trigger_argtype, on_receive
)
trigger_lambda = await cg.process_lambda(
trigger.trigger(literal(trigger_argname)), trigger_argtype
trigger.trigger(
cg.std_vector.template(cg.uint8)(
MockObj(trigger_argname).begin(),
MockObj(trigger_argname).end(),
)
),
listener_argtype,
)
cg.add(var.add_listener(trigger_lambda))
cg.add(var.set_should_listen())

View File

@@ -12,7 +12,7 @@ bool UDPTransport::should_send() { return network::is_connected(); }
void UDPTransport::setup() {
PacketTransport::setup();
if (!this->providers_.empty() || this->is_encrypted_()) {
this->parent_->add_listener([this](std::vector<uint8_t> &buf) { this->process_(buf); });
this->parent_->add_listener([this](std::span<const uint8_t> data) { this->process_(data); });
}
}

View File

@@ -103,8 +103,8 @@ void UDPComponent::setup() {
}
void UDPComponent::loop() {
auto buf = std::vector<uint8_t>(MAX_PACKET_SIZE);
if (this->should_listen_) {
std::array<uint8_t, MAX_PACKET_SIZE> buf;
for (;;) {
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
auto len = this->listen_socket_->read(buf.data(), buf.size());
@@ -116,9 +116,9 @@ void UDPComponent::loop() {
#endif
if (len <= 0)
break;
buf.resize(len);
ESP_LOGV(TAG, "Received packet of length %zu", len);
this->packet_listeners_.call(buf);
size_t packet_len = static_cast<size_t>(len);
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
}
}
}

View File

@@ -10,7 +10,9 @@
#ifdef USE_SOCKET_IMPL_LWIP_TCP
#include <WiFiUdp.h>
#endif
#include <array>
#include <initializer_list>
#include <span>
#include <vector>
namespace esphome::udp {
@@ -26,7 +28,7 @@ class UDPComponent : public Component {
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
void set_should_broadcast() { this->should_broadcast_ = true; }
void set_should_listen() { this->should_listen_ = true; }
void add_listener(std::function<void(std::vector<uint8_t> &)> &&listener) {
void add_listener(std::function<void(std::span<const uint8_t>)> &&listener) {
this->packet_listeners_.add(std::move(listener));
}
void setup() override;
@@ -41,7 +43,7 @@ class UDPComponent : public Component {
uint16_t broadcast_port_{};
bool should_broadcast_{};
bool should_listen_{};
CallbackManager<void(std::vector<uint8_t> &)> packet_listeners_{};
CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr;

View File

@@ -371,12 +371,7 @@ async def to_code(config):
if on_timer_tick := config.get(CONF_ON_TIMER_TICK):
await automation.build_automation(
var.get_timer_tick_trigger(),
[
(
cg.std_vector.template(Timer).operator("const").operator("ref"),
"timers",
)
],
[(cg.std_vector.template(Timer), "timers")],
on_timer_tick,
)
has_timers = True

View File

@@ -859,43 +859,35 @@ void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) {
}
void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse &msg) {
// Find existing timer or add a new one
auto it = this->timers_.begin();
for (; it != this->timers_.end(); ++it) {
if (it->id == msg.timer_id)
break;
}
if (it == this->timers_.end()) {
this->timers_.push_back({});
it = this->timers_.end() - 1;
}
it->id = msg.timer_id;
it->name = msg.name;
it->total_seconds = msg.total_seconds;
it->seconds_left = msg.seconds_left;
it->is_active = msg.is_active;
Timer timer = {
.id = msg.timer_id,
.name = msg.name,
.total_seconds = msg.total_seconds,
.seconds_left = msg.seconds_left,
.is_active = msg.is_active,
};
this->timers_[timer.id] = timer;
char timer_buf[Timer::TO_STR_BUFFER_SIZE];
ESP_LOGD(TAG,
"Timer Event\n"
" Type: %" PRId32 "\n"
" %s",
msg.event_type, it->to_str(timer_buf));
msg.event_type, timer.to_str(timer_buf));
switch (msg.event_type) {
case api::enums::VOICE_ASSISTANT_TIMER_STARTED:
this->timer_started_trigger_.trigger(*it);
this->timer_started_trigger_.trigger(timer);
break;
case api::enums::VOICE_ASSISTANT_TIMER_UPDATED:
this->timer_updated_trigger_.trigger(*it);
this->timer_updated_trigger_.trigger(timer);
break;
case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED:
this->timer_cancelled_trigger_.trigger(*it);
this->timers_.erase(it);
this->timer_cancelled_trigger_.trigger(timer);
this->timers_.erase(timer.id);
break;
case api::enums::VOICE_ASSISTANT_TIMER_FINISHED:
this->timer_finished_trigger_.trigger(*it);
this->timers_.erase(it);
this->timer_finished_trigger_.trigger(timer);
this->timers_.erase(timer.id);
break;
}
@@ -909,12 +901,16 @@ void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse
}
void VoiceAssistant::timer_tick_() {
for (auto &timer : this->timers_) {
std::vector<Timer> res;
res.reserve(this->timers_.size());
for (auto &pair : this->timers_) {
auto &timer = pair.second;
if (timer.is_active && timer.seconds_left > 0) {
timer.seconds_left--;
}
res.push_back(timer);
}
this->timer_tick_trigger_.trigger(this->timers_);
this->timer_tick_trigger_.trigger(res);
}
void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) {

View File

@@ -24,6 +24,7 @@
#include "esphome/components/socket/socket.h"
#include <span>
#include <unordered_map>
#include <vector>
namespace esphome {
@@ -225,9 +226,9 @@ class VoiceAssistant : public Component {
Trigger<Timer> *get_timer_updated_trigger() { return &this->timer_updated_trigger_; }
Trigger<Timer> *get_timer_cancelled_trigger() { return &this->timer_cancelled_trigger_; }
Trigger<Timer> *get_timer_finished_trigger() { return &this->timer_finished_trigger_; }
Trigger<const std::vector<Timer> &> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; }
Trigger<std::vector<Timer>> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; }
void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; }
const std::vector<Timer> &get_timers() const { return this->timers_; }
const std::unordered_map<std::string, Timer> &get_timers() const { return this->timers_; }
protected:
bool allocate_buffers_();
@@ -266,13 +267,13 @@ class VoiceAssistant : public Component {
api::APIConnection *api_client_{nullptr};
std::vector<Timer> timers_;
std::unordered_map<std::string, Timer> timers_;
void timer_tick_();
Trigger<Timer> timer_started_trigger_;
Trigger<Timer> timer_finished_trigger_;
Trigger<Timer> timer_updated_trigger_;
Trigger<Timer> timer_cancelled_trigger_;
Trigger<const std::vector<Timer> &> timer_tick_trigger_;
Trigger<std::vector<Timer>> timer_tick_trigger_;
bool has_timers_{false};
bool timer_tick_running_{false};

View File

@@ -65,6 +65,7 @@ WaterHeaterCall &WaterHeaterCall::set_away(bool away) {
} else {
this->state_ &= ~WATER_HEATER_STATE_AWAY;
}
this->state_mask_ |= WATER_HEATER_STATE_AWAY;
return *this;
}
@@ -74,6 +75,7 @@ WaterHeaterCall &WaterHeaterCall::set_on(bool on) {
} else {
this->state_ &= ~WATER_HEATER_STATE_ON;
}
this->state_mask_ |= WATER_HEATER_STATE_ON;
return *this;
}
@@ -92,11 +94,11 @@ void WaterHeaterCall::perform() {
if (!std::isnan(this->target_temperature_high_)) {
ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_);
}
if (this->state_ & WATER_HEATER_STATE_AWAY) {
ESP_LOGD(TAG, " Away: YES");
if (this->state_mask_ & WATER_HEATER_STATE_AWAY) {
ESP_LOGD(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO");
}
if (this->state_ & WATER_HEATER_STATE_ON) {
ESP_LOGD(TAG, " On: YES");
if (this->state_mask_ & WATER_HEATER_STATE_ON) {
ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO");
}
this->parent_->control(*this);
}
@@ -137,13 +139,17 @@ void WaterHeaterCall::validate_() {
this->target_temperature_high_ = NAN;
}
}
if ((this->state_ & WATER_HEATER_STATE_AWAY) && !traits.get_supports_away_mode()) {
ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str());
if (!traits.get_supports_away_mode()) {
if (this->state_ & WATER_HEATER_STATE_AWAY) {
ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str());
}
this->state_ &= ~WATER_HEATER_STATE_AWAY;
this->state_mask_ &= ~WATER_HEATER_STATE_AWAY;
}
// If ON/OFF not supported, device is always on - clear the flag silently
if (!traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) {
this->state_ &= ~WATER_HEATER_STATE_ON;
this->state_mask_ &= ~WATER_HEATER_STATE_ON;
}
}

View File

@@ -91,6 +91,8 @@ class WaterHeaterCall {
float get_target_temperature_high() const { return this->target_temperature_high_; }
/// Get state flags value
uint32_t get_state() const { return this->state_; }
/// Get mask of state flags that are being changed
uint32_t get_state_mask() const { return this->state_mask_; }
protected:
void validate_();
@@ -100,6 +102,7 @@ class WaterHeaterCall {
float target_temperature_low_{NAN};
float target_temperature_high_{NAN};
uint32_t state_{0};
uint32_t state_mask_{0};
};
struct WaterHeaterCallInternal : public WaterHeaterCall {
@@ -111,6 +114,7 @@ struct WaterHeaterCallInternal : public WaterHeaterCall {
this->target_temperature_low_ = restore.target_temperature_low_;
this->target_temperature_high_ = restore.target_temperature_high_;
this->state_ = restore.state_;
this->state_mask_ = restore.state_mask_;
return *this;
}
};

View File

@@ -6,8 +6,7 @@
#include <cstring>
#include "multipart_parser.h"
namespace esphome {
namespace web_server_idf {
namespace esphome::web_server_idf {
static const char *const TAG = "multipart";
@@ -249,6 +248,5 @@ std::string str_trim(const std::string &str) {
return str.substr(start, end - start + 1);
}
} // namespace web_server_idf
} // namespace esphome
} // namespace esphome::web_server_idf
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)

View File

@@ -10,8 +10,7 @@
#include <string>
#include <utility>
namespace esphome {
namespace web_server_idf {
namespace esphome::web_server_idf {
// Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads
class MultipartReader {
@@ -81,6 +80,5 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st
// Trim whitespace from both ends of a string
std::string str_trim(const std::string &str);
} // namespace web_server_idf
} // namespace esphome
} // namespace esphome::web_server_idf
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)

View File

@@ -8,8 +8,7 @@
#include "utils.h"
namespace esphome {
namespace web_server_idf {
namespace esphome::web_server_idf {
static const char *const TAG = "web_server_idf_utils";
@@ -119,6 +118,5 @@ const char *stristr(const char *haystack, const char *needle) {
return nullptr;
}
} // namespace web_server_idf
} // namespace esphome
} // namespace esphome::web_server_idf
#endif // USE_ESP32

View File

@@ -5,8 +5,7 @@
#include <string>
#include "esphome/core/helpers.h"
namespace esphome {
namespace web_server_idf {
namespace esphome::web_server_idf {
/// Decode URL-encoded string in-place (e.g., %20 -> space, + -> space)
/// Returns the new length of the decoded string
@@ -29,6 +28,5 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n);
// Case-insensitive string search (like strstr but case-insensitive)
const char *stristr(const char *haystack, const char *needle);
} // namespace web_server_idf
} // namespace esphome
} // namespace esphome::web_server_idf
#endif // USE_ESP32

View File

@@ -30,8 +30,7 @@
#include <cerrno>
#include <sys/socket.h>
namespace esphome {
namespace web_server_idf {
namespace esphome::web_server_idf {
#ifndef HTTPD_409
#define HTTPD_409 "409 Conflict"
@@ -258,8 +257,6 @@ StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) co
return StringRef(buffer.data(), decoded_len);
}
std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); }
void AsyncWebServerRequest::send(AsyncWebServerResponse *response) {
httpd_resp_send(*this, response->get_content_data(), response->get_content_size());
}
@@ -897,7 +894,6 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
}
#endif // USE_WEBSERVER_OTA
} // namespace web_server_idf
} // namespace esphome
} // namespace esphome::web_server_idf
#endif // !defined(USE_ESP32)

View File

@@ -121,7 +121,6 @@ class AsyncWebServerRequest {
char buffer[URL_BUF_SIZE];
return std::string(this->url_to(buffer));
}
std::string host() const;
// NOLINTNEXTLINE(readability-identifier-naming)
size_t contentLength() const { return this->req_->content_len; }

View File

@@ -349,7 +349,7 @@ bool WiFiComponent::needs_scan_results_() const {
return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
}
bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const {
// Check if this SSID is configured as hidden
// If explicitly marked hidden, we should always try hidden mode regardless of scan results
for (const auto &conf : this->sta_) {
@@ -960,9 +960,12 @@ WiFiAP WiFiComponent::get_sta() const {
return config ? *config : WiFiAP{};
}
void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
this->save_wifi_sta(ssid.c_str(), password.c_str());
}
void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
this->pref_.save(&save);
// ensure it's written immediately
global_preferences->sync();
@@ -1825,11 +1828,11 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
}
// Get SSID for logging (use pointer to avoid copy)
const std::string *ssid = nullptr;
const char *ssid = nullptr;
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
ssid = &this->scan_result_[0].get_ssid();
ssid = this->scan_result_[0].get_ssid().c_str();
} else if (const WiFiAP *config = this->get_selected_sta_()) {
ssid = &config->get_ssid();
ssid = config->get_ssid().c_str();
}
// Only decrease priority on the last attempt for this phase
@@ -1849,8 +1852,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
}
char bssid_s[18];
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",
ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
bssid_s, old_priority, new_priority);
// After adjusting priority, check if all priorities are now at minimum
// If so, clear the vector to save memory and reset for fresh start
@@ -2098,10 +2101,14 @@ void WiFiComponent::save_fast_connect_settings_() {
}
#endif
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
void WiFiAP::clear_bssid() { this->bssid_ = {}; }
void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
void WiFiAP::set_password(const std::string &password) {
this->password_ = CompactString(password.c_str(), password.size());
}
void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
#ifdef USE_WIFI_WPA2_EAP
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif
@@ -2111,10 +2118,8 @@ void WiFiAP::clear_channel() { this->channel_ = 0; }
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
#endif
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
const std::string &WiFiAP::get_password() const { return this->password_; }
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
#endif
@@ -2125,12 +2130,12 @@ const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip
#endif
bool WiFiAP::get_hidden() const { return this->hidden_; }
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
bool is_hidden)
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
bool with_auth, bool is_hidden)
: bssid_(bssid),
channel_(channel),
rssi_(rssi),
ssid_(std::move(ssid)),
ssid_(ssid, ssid_len),
with_auth_(with_auth),
is_hidden_(is_hidden) {}
bool WiFiScanResult::matches(const WiFiAP &config) const {
@@ -2173,7 +2178,6 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
bool WiFiScanResult::get_matches() const { return this->matches_; }
void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
@@ -2284,7 +2288,7 @@ void WiFiComponent::process_roaming_scan_() {
for (const auto &result : this->scan_result_) {
// Must be same SSID, different BSSID
if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid)
if (result.get_ssid() != current_ssid.c_str() || result.get_bssid() == current_bssid)
continue;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE

View File

@@ -175,9 +175,13 @@ template<typename T> using wifi_scan_vector_t = FixedVector<T>;
class WiFiAP {
public:
void set_ssid(const std::string &ssid);
void set_ssid(const char *ssid);
void set_ssid(const CompactString &ssid) { this->ssid_ = ssid; }
void set_bssid(const bssid_t &bssid);
void clear_bssid();
void set_password(const std::string &password);
void set_password(const char *password);
void set_password(const CompactString &password) { this->password_ = password; }
#ifdef USE_WIFI_WPA2_EAP
void set_eap(optional<EAPAuth> eap_auth);
#endif // USE_WIFI_WPA2_EAP
@@ -188,10 +192,10 @@ class WiFiAP {
void set_manual_ip(optional<ManualIP> manual_ip);
#endif
void set_hidden(bool hidden);
const std::string &get_ssid() const;
const CompactString &get_ssid() const { return this->ssid_; }
const CompactString &get_password() const { return this->password_; }
const bssid_t &get_bssid() const;
bool has_bssid() const;
const std::string &get_password() const;
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &get_eap() const;
#endif // USE_WIFI_WPA2_EAP
@@ -204,8 +208,8 @@ class WiFiAP {
bool get_hidden() const;
protected:
std::string ssid_;
std::string password_;
CompactString ssid_;
CompactString password_;
#ifdef USE_WIFI_WPA2_EAP
optional<EAPAuth> eap_;
#endif // USE_WIFI_WPA2_EAP
@@ -221,14 +225,15 @@ class WiFiAP {
class WiFiScanResult {
public:
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth,
bool is_hidden);
bool matches(const WiFiAP &config) const;
bool get_matches() const;
void set_matches(bool matches);
const bssid_t &get_bssid() const;
const std::string &get_ssid() const;
const CompactString &get_ssid() const { return this->ssid_; }
uint8_t get_channel() const;
int8_t get_rssi() const;
bool get_with_auth() const;
@@ -242,7 +247,7 @@ class WiFiScanResult {
bssid_t bssid_;
uint8_t channel_;
int8_t rssi_;
std::string ssid_;
CompactString ssid_;
int8_t priority_{0};
bool matches_{false};
bool with_auth_;
@@ -381,6 +386,10 @@ class WiFiComponent : public Component {
void set_passive_scan(bool passive);
void save_wifi_sta(const std::string &ssid, const std::string &password);
void save_wifi_sta(const char *ssid, const char *password);
void save_wifi_sta(const CompactString &ssid, const CompactString &password) {
this->save_wifi_sta(ssid.c_str(), password.c_str());
}
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
@@ -545,7 +554,7 @@ class WiFiComponent : public Component {
int8_t find_first_non_hidden_index_() const;
/// Check if an SSID was seen in the most recent scan results
/// Used to skip hidden mode for SSIDs we know are visible
bool ssid_was_seen_in_scan_(const std::string &ssid) const;
bool ssid_was_seen_in_scan_(const CompactString &ssid) const;
/// Check if full scan results are needed (captive portal active, improv, listeners)
bool needs_full_scan_results_() const;
/// Check if network matches any configured network (for scan result filtering)

View File

@@ -738,8 +738,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
this->scan_result_.emplace_back(
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr,
it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
} else {
this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
}

View File

@@ -864,8 +864,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) {
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
std::string ssid(ssid_cstr);
this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi,
this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
} else {
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);

View File

@@ -688,7 +688,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
auto &ap = scan->ap[i];
this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
ap.bssid.addr[4], ap.bssid.addr[5]},
std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
ssid_cstr[0] == '\0');
} else {
auto &ap = scan->ap[i];

View File

@@ -149,9 +149,8 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re
bssid_t bssid;
std::copy(result->bssid, result->bssid + 6, bssid.begin());
std::string ssid(ssid_cstr);
WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN,
ssid_cstr[0] == '\0');
WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi,
result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0');
if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
this->scan_result_.push_back(res);
}

View File

@@ -89,7 +89,7 @@ void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t<wi
for (const auto &scan : results) {
if (scan.get_is_hidden())
continue;
const std::string &ssid = scan.get_ssid();
const auto &ssid = scan.get_ssid();
// Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9
if (ptr + ssid.size() + 9 > end)
break;

View File

@@ -13,6 +13,7 @@
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <new>
#ifdef USE_ESP32
#include "rom/crc.h"
@@ -858,4 +859,60 @@ void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) {
;
}
// CompactString implementation
CompactString::CompactString(const char *str, size_t len) {
if (len > MAX_LENGTH) {
len = MAX_LENGTH; // Clamp to max valid length
}
this->length_ = len;
if (len <= INLINE_CAPACITY) {
// Store inline with null terminator
this->is_heap_ = 0;
if (len > 0) {
std::memcpy(this->storage_, str, len);
}
this->storage_[len] = '\0';
} else {
// Heap allocate with null terminator
this->is_heap_ = 1;
char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory)
std::memcpy(heap_data, str, len);
heap_data[len] = '\0';
this->set_heap_ptr_(heap_data);
}
}
CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {}
CompactString &CompactString::operator=(const CompactString &other) {
if (this != &other) {
this->~CompactString();
new (this) CompactString(other);
}
return *this;
}
CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) {
// Copy full storage (includes null terminator for inline, or pointer for heap)
std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1);
other.length_ = 0;
other.is_heap_ = 0;
other.storage_[0] = '\0';
}
CompactString &CompactString::operator=(CompactString &&other) noexcept {
if (this != &other) {
this->~CompactString();
new (this) CompactString(std::move(other));
}
return *this;
}
CompactString::~CompactString() {
if (this->is_heap_) {
delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory)
}
}
} // namespace esphome

View File

@@ -1787,4 +1787,58 @@ template<typename T, enable_if_t<std::is_pointer<T *>::value, int> = 0> T &id(T
///@}
/// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated.
class CompactString {
public:
static constexpr uint8_t MAX_LENGTH = 127;
static constexpr uint8_t INLINE_CAPACITY = 18; // 18 chars + null terminator fits in 19 bytes
static constexpr uint8_t BUFFER_SIZE = MAX_LENGTH + 1; // For external buffer (128 bytes)
CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; }
CompactString(const char *str, size_t len);
CompactString(const CompactString &other);
CompactString(CompactString &&other) noexcept;
CompactString &operator=(const CompactString &other);
CompactString &operator=(CompactString &&other) noexcept;
~CompactString();
const char *data() const { return this->is_heap_ ? this->get_heap_ptr_() : this->storage_; }
const char *c_str() const { return this->data(); } // Always null-terminated
size_t size() const { return this->length_; }
bool empty() const { return this->length_ == 0; }
// Implicit conversion to std::string for backwards compatibility
operator std::string() const { return std::string(this->data(), this->size()); }
bool operator==(const CompactString &other) const {
return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0;
}
bool operator==(const std::string &other) const {
return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0;
}
bool operator==(const char *other) const {
return this->size() == std::strlen(other) && std::memcmp(this->data(), other, this->size()) == 0;
}
bool operator!=(const CompactString &other) const { return !(*this == other); }
bool operator!=(const std::string &other) const { return !(*this == other); }
bool operator!=(const char *other) const { return !(*this == other); }
protected:
char *get_heap_ptr_() const {
char *ptr;
std::memcpy(&ptr, this->storage_, sizeof(ptr));
return ptr;
}
void set_heap_ptr_(char *ptr) { std::memcpy(this->storage_, &ptr, sizeof(ptr)); }
// Storage for string data. When is_heap_=0, contains the string directly (null-terminated).
// When is_heap_=1, first sizeof(char*) bytes contain pointer to heap allocation.
char storage_[INLINE_CAPACITY + 1]; // 19 bytes: 18 chars + null terminator
uint8_t length_ : 7; // String length (0-127)
uint8_t is_heap_ : 1; // 1 if using heap pointer, 0 if using inline storage
// Total size: 20 bytes (19 bytes storage + 1 byte bitfields)
};
static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes");
} // namespace esphome

View File

@@ -12,6 +12,7 @@ std_shared_ptr = std_ns.class_("shared_ptr")
std_string = std_ns.class_("string")
std_string_ref = std_ns.namespace("string &")
std_vector = std_ns.class_("vector")
std_span = std_ns.class_("span")
uint8 = global_ns.namespace("uint8_t")
uint16 = global_ns.namespace("uint16_t")
uint32 = global_ns.namespace("uint32_t")

View File

@@ -1054,17 +1054,26 @@ class DownloadBinaryRequestHandler(BaseHandler):
# fallback to type=, but prioritize file=
file_name = self.get_argument("type", None)
file_name = self.get_argument("file", file_name)
if file_name is None:
if file_name is None or not file_name.strip():
self.send_error(400)
return
file_name = file_name.replace("..", "").lstrip("/")
# get requested download name, or build it based on filename
download_name = self.get_argument(
"download",
f"{storage_json.name}-{file_name}",
)
path = storage_json.firmware_bin_path.parent.joinpath(file_name)
if storage_json.firmware_bin_path is None:
self.send_error(404)
return
base_dir = storage_json.firmware_bin_path.parent.resolve()
path = base_dir.joinpath(file_name).resolve()
try:
path.relative_to(base_dir)
except ValueError:
self.send_error(403)
return
if not path.is_file():
args = ["esphome", "idedata", settings.rel_path(configuration)]
@@ -1078,7 +1087,7 @@ class DownloadBinaryRequestHandler(BaseHandler):
found = False
for image in idedata.extra_flash_images:
if image.path.endswith(file_name):
if image.path.as_posix().endswith(file_name):
path = image.path
download_name = file_name
found = True

View File

@@ -1,6 +1,5 @@
import base64
from pathlib import Path
import random
import secrets
import string
from typing import Literal, NotRequired, TypedDict, Unpack
@@ -130,7 +129,7 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str:
if len(ap_name) > 32:
ap_name = ap_name_base
kwargs["fallback_name"] = ap_name
kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12))
kwargs["fallback_psk"] = "".join(secrets.choice(letters) for _ in range(12))
base = BASE_CONFIG_FRIENDLY if kwargs.get("friendly_name") else BASE_CONFIG

View File

@@ -2270,10 +2270,13 @@ SOURCE_NAMES = {
SOURCE_CLIENT: "SOURCE_CLIENT",
}
RECEIVE_CASES: dict[int, tuple[str, str | None]] = {}
RECEIVE_CASES: dict[int, tuple[str, str | None, str]] = {}
ifdefs: dict[str, str] = {}
# Track messages with no fields (empty messages) for parameter elision
EMPTY_MESSAGES: set[str] = set()
def get_opt(
desc: descriptor.DescriptorProto,
@@ -2504,26 +2507,26 @@ def build_service_message_type(
# Only add ifdef when we're actually generating content
if ifdef is not None:
hout += f"#ifdef {ifdef}\n"
# Generate receive
# Generate receive handler and switch case
func = f"on_{snake}"
hout += f"virtual void {func}(const {mt.name} &value){{}};\n"
case = ""
case += f"{mt.name} msg;\n"
# Check if this message has any fields (excluding deprecated ones)
has_fields = any(not field.options.deprecated for field in mt.field)
if has_fields:
# Normal case: decode the message
is_empty = not has_fields
if is_empty:
EMPTY_MESSAGES.add(mt.name)
hout += f"virtual void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n"
case = ""
if not is_empty:
case += f"{mt.name} msg;\n"
case += "msg.decode(msg_data, msg_size);\n"
else:
# Empty message optimization: skip decode since there are no fields
case += "// Empty message: no decode needed\n"
if log:
case += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
case += f'this->log_receive_message_(LOG_STR("{func}"), msg);\n'
if is_empty:
case += f'this->log_receive_message_(LOG_STR("{func}"));\n'
else:
case += f'this->log_receive_message_(LOG_STR("{func}"), msg);\n'
case += "#endif\n"
case += f"this->{func}(msg);\n"
case += f"this->{func}({'msg' if not is_empty else ''});\n"
case += "break;"
# Store the message name and ifdef with the case for later use
RECEIVE_CASES[id_] = (case, ifdef, mt.name)
# Only close ifdef if we opened it
@@ -2839,6 +2842,7 @@ static const char *const TAG = "api.service";
hpp += (
" void log_receive_message_(const LogString *name, const ProtoMessage &msg);\n"
)
hpp += " void log_receive_message_(const LogString *name);\n"
hpp += " public:\n"
hpp += "#endif\n\n"
@@ -2862,6 +2866,9 @@ static const char *const TAG = "api.service";
cpp += " DumpBuffer dump_buf;\n"
cpp += ' ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf));\n'
cpp += "}\n"
cpp += f"void {class_name}::log_receive_message_(const LogString *name) {{\n"
cpp += ' ESP_LOGVV(TAG, "%s: {}", LOG_STR_ARG(name));\n'
cpp += "}\n"
cpp += "#endif\n\n"
for mt in file.message_type:
@@ -2929,22 +2936,22 @@ static const char *const TAG = "api.service";
hpp_protected += f"#ifdef {ifdef}\n"
cpp += f"#ifdef {ifdef}\n"
hpp_protected += f" void {on_func}(const {inp} &msg) override;\n"
is_empty = inp in EMPTY_MESSAGES
param = "" if is_empty else f"const {inp} &msg"
arg = "" if is_empty else "msg"
# For non-void methods, generate a send_ method instead of return-by-value
hpp_protected += f" void {on_func}({param}) override;\n"
if is_void:
hpp += f" virtual void {func}(const {inp} &msg) = 0;\n"
hpp += f" virtual void {func}({param}) = 0;\n"
else:
hpp += f" virtual bool send_{func}_response(const {inp} &msg) = 0;\n"
hpp += f" virtual bool send_{func}_response({param}) = 0;\n"
cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n"
# No authentication check here - it's done in read_message
cpp += f"void {class_name}::{on_func}({param}) {{\n"
body = ""
if is_void:
body += f"this->{func}(msg);\n"
body += f"this->{func}({arg});\n"
else:
body += f"if (!this->send_{func}_response(msg)) {{\n"
body += f"if (!this->send_{func}_response({arg})) {{\n"
body += " this->on_fatal_error();\n"
body += "}\n"

View File

@@ -20,6 +20,8 @@ lvgl:
- id: lvgl_0
default_font: space16
displays: sdl0
top_layer:
- id: lvgl_1
displays: sdl1
on_idle:

View File

@@ -68,24 +68,3 @@ voice_assistant:
- logger.log:
format: "Voice assistant error - code %s, message: %s"
args: [code.c_str(), message.c_str()]
on_timer_started:
- logger.log:
format: "Timer started: %s"
args: [timer.id.c_str()]
on_timer_updated:
- logger.log:
format: "Timer updated: %s"
args: [timer.id.c_str()]
on_timer_cancelled:
- logger.log:
format: "Timer cancelled: %s"
args: [timer.id.c_str()]
on_timer_finished:
- logger.log:
format: "Timer finished: %s"
args: [timer.id.c_str()]
on_timer_tick:
- lambda: |-
for (auto &timer : timers) {
ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left);
}

View File

@@ -58,24 +58,3 @@ voice_assistant:
- logger.log:
format: "Voice assistant error - code %s, message: %s"
args: [code.c_str(), message.c_str()]
on_timer_started:
- logger.log:
format: "Timer started: %s"
args: [timer.id.c_str()]
on_timer_updated:
- logger.log:
format: "Timer updated: %s"
args: [timer.id.c_str()]
on_timer_cancelled:
- logger.log:
format: "Timer cancelled: %s"
args: [timer.id.c_str()]
on_timer_finished:
- logger.log:
format: "Timer finished: %s"
args: [timer.id.c_str()]
on_timer_tick:
- lambda: |-
for (auto &timer : timers) {
ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left);
}

View File

@@ -8,6 +8,7 @@ import gzip
import json
import os
from pathlib import Path
import sys
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
@@ -421,7 +422,7 @@ async def test_download_binary_handler_idedata_fallback(
# Mock idedata response
mock_image = Mock()
mock_image.path = str(bootloader_file)
mock_image.path = bootloader_file
mock_idedata_instance = Mock()
mock_idedata_instance.extra_flash_images = [mock_image]
mock_idedata.return_value = mock_idedata_instance
@@ -528,14 +529,22 @@ async def test_download_binary_handler_subdirectory_file_url_encoded(
@pytest.mark.asyncio
@pytest.mark.usefixtures("mock_ext_storage_path")
@pytest.mark.parametrize(
"attack_path",
("attack_path", "expected_code"),
[
pytest.param("../../../secrets.yaml", id="basic_traversal"),
pytest.param("..%2F..%2F..%2Fsecrets.yaml", id="url_encoded"),
pytest.param("zephyr/../../../secrets.yaml", id="traversal_with_prefix"),
pytest.param("/etc/passwd", id="absolute_path"),
pytest.param("//etc/passwd", id="double_slash_absolute"),
pytest.param("....//secrets.yaml", id="multiple_dots"),
pytest.param("../../../secrets.yaml", 403, id="basic_traversal"),
pytest.param("..%2F..%2F..%2Fsecrets.yaml", 403, id="url_encoded"),
pytest.param("zephyr/../../../secrets.yaml", 403, id="traversal_with_prefix"),
pytest.param("/etc/passwd", 403, id="absolute_path"),
pytest.param("//etc/passwd", 403, id="double_slash_absolute"),
pytest.param(
"....//secrets.yaml",
# On Windows, Path.resolve() treats "..." and "...." as parent
# traversal (like ".."), so the path escapes base_dir -> 403.
# On Unix, "...." is a literal directory name that stays inside
# base_dir but doesn't exist -> 404.
403 if sys.platform == "win32" else 404,
id="multiple_dots",
),
],
)
async def test_download_binary_handler_path_traversal_protection(
@@ -543,11 +552,14 @@ async def test_download_binary_handler_path_traversal_protection(
tmp_path: Path,
mock_storage_json: MagicMock,
attack_path: str,
expected_code: int,
) -> None:
"""Test that DownloadBinaryRequestHandler prevents path traversal attacks.
Verifies that attempts to use '..' in file paths are sanitized to prevent
accessing files outside the build directory. Tests multiple attack vectors.
Verifies that attempts to escape the build directory via '..' are rejected
using resolve()/relative_to() validation. Tests multiple attack vectors.
Real traversals that escape the base directory get 403. Paths like '....'
that resolve inside the base directory but don't exist get 404.
"""
# Create build structure
build_dir = get_build_path(tmp_path, "test")
@@ -565,14 +577,67 @@ async def test_download_binary_handler_path_traversal_protection(
mock_storage.firmware_bin_path = firmware_file
mock_storage_json.load.return_value = mock_storage
# Attempt path traversal attack - should be blocked
with pytest.raises(HTTPClientError) as exc_info:
# Mock async_run_system_command so paths that pass validation but don't exist
# return 404 deterministically without spawning a real subprocess.
with (
patch(
"esphome.dashboard.web_server.async_run_system_command",
new_callable=AsyncMock,
return_value=(2, "", ""),
),
pytest.raises(HTTPClientError) as exc_info,
):
await dashboard.fetch(
f"/download.bin?configuration=test.yaml&file={attack_path}",
method="GET",
)
# Should get 404 (file not found after sanitization) or 500 (idedata fails)
assert exc_info.value.code in (404, 500)
assert exc_info.value.code == expected_code
@pytest.mark.asyncio
@pytest.mark.usefixtures("mock_ext_storage_path")
async def test_download_binary_handler_no_firmware_bin_path(
dashboard: DashboardTestHelper,
mock_storage_json: MagicMock,
) -> None:
"""Test that download returns 404 when firmware_bin_path is None.
This covers configs created by StorageJSON.from_wizard() where no
firmware has been compiled yet.
"""
mock_storage = Mock()
mock_storage.name = "test_device"
mock_storage.firmware_bin_path = None
mock_storage_json.load.return_value = mock_storage
with pytest.raises(HTTPClientError) as exc_info:
await dashboard.fetch(
"/download.bin?configuration=test.yaml&file=firmware.bin",
method="GET",
)
assert exc_info.value.code == 404
@pytest.mark.asyncio
@pytest.mark.usefixtures("mock_ext_storage_path")
@pytest.mark.parametrize("file_value", ["", "%20%20", "%20"])
async def test_download_binary_handler_empty_file_name(
dashboard: DashboardTestHelper,
mock_storage_json: MagicMock,
file_value: str,
) -> None:
"""Test that download returns 400 for empty or whitespace-only file names."""
mock_storage = Mock()
mock_storage.name = "test_device"
mock_storage.firmware_bin_path = Path("/fake/firmware.bin")
mock_storage_json.load.return_value = mock_storage
with pytest.raises(HTTPClientError) as exc_info:
await dashboard.fetch(
f"/download.bin?configuration=test.yaml&file={file_value}",
method="GET",
)
assert exc_info.value.code == 400
@pytest.mark.asyncio

View File

@@ -93,23 +93,34 @@ async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]]
sock.close()
def _get_free_udp_port() -> int:
"""Get a free UDP port by binding to port 0 and releasing."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
return port
@pytest.mark.asyncio
async def test_udp_send_receive(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test UDP component can send messages with multiple addresses configured."""
# Track log lines to verify dump_config output
"""Test UDP component can send and receive messages."""
log_lines: list[str] = []
receive_event = asyncio.Event()
def on_log_line(line: str) -> None:
log_lines.append(line)
if "Received UDP:" in line:
receive_event.set()
async with udp_listener() as (udp_port, receiver):
# Replace placeholders in the config
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1))
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port))
async with udp_listener() as (broadcast_port, receiver):
listen_port = _get_free_udp_port()
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(listen_port))
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(broadcast_port))
async with (
run_compiled(config, line_callback=on_log_line),
@@ -169,3 +180,19 @@ async def test_udp_send_receive(
assert "Address: 127.0.0.2" in log_text, (
f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}"
)
# Test receiving a UDP packet (exercises on_receive with std::span)
test_payload = b"TEST_RECEIVE_UDP"
send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
send_sock.sendto(test_payload, ("127.0.0.1", listen_port))
finally:
send_sock.close()
try:
await asyncio.wait_for(receive_event.wait(), timeout=5.0)
except TimeoutError:
pytest.fail(
f"on_receive did not fire. Expected 'Received UDP:' in logs. "
f"Last log lines: {log_lines[-20:]}"
)

View File

@@ -2,7 +2,7 @@
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
from pytest import MonkeyPatch
@@ -632,3 +632,14 @@ def test_wizard_accepts_rpipico_board(tmp_path: Path, monkeypatch: MonkeyPatch):
# rpipico doesn't support WiFi, so no api_encryption_key or ota_password
assert "api_encryption_key" not in call_kwargs
assert "ota_password" not in call_kwargs
def test_fallback_psk_uses_secrets_choice(
default_config: dict[str, Any],
) -> None:
"""Test that fallback PSK is generated using secrets.choice."""
with patch("esphome.wizard.secrets.choice", return_value="X") as mock_choice:
config = wz.wizard_file(**default_config)
assert 'password: "XXXXXXXXXXXX"' in config
assert mock_choice.call_count == 12