mirror of
https://github.com/esphome/esphome.git
synced 2026-01-30 16:32:10 -07:00
Compare commits
5 Commits
http_reque
...
esp32_ard_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4be547d41 | ||
|
|
6e2f7a196f | ||
|
|
e2182b6227 | ||
|
|
77fa58541f | ||
|
|
49840ed4fa |
@@ -1 +1 @@
|
||||
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
|
||||
cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab
|
||||
|
||||
@@ -89,8 +89,9 @@ async def to_code(config):
|
||||
var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds)
|
||||
)
|
||||
|
||||
# Although this component does not use SPI, the BSEC library requires the SPI library
|
||||
# Although this component does not use SPI/Wire directly, the BSEC library requires them
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library("Wire", None)
|
||||
|
||||
cg.add_define("USE_BSEC")
|
||||
cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480")
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from esphome import yaml_util
|
||||
import esphome.codegen as cg
|
||||
@@ -50,6 +51,7 @@ from esphome.writer import clean_cmake_cache
|
||||
|
||||
from .boards import BOARDS, STANDARD_BOARDS
|
||||
from .const import ( # noqa
|
||||
KEY_ARDUINO_LIBRARIES,
|
||||
KEY_BOARD,
|
||||
KEY_COMPONENTS,
|
||||
KEY_ESP32,
|
||||
@@ -146,6 +148,168 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
|
||||
)
|
||||
|
||||
# Additional IDF managed components to exclude for Arduino framework builds
|
||||
# These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome
|
||||
# Note: Component names include the namespace prefix (e.g., "espressif__cbor") because
|
||||
# that's how managed components are registered in the IDF build system
|
||||
# List includes direct dependencies from arduino-esp32/idf_component.yml
|
||||
# plus transitive dependencies from RainMaker/Insights (except espressif/mdns which we need)
|
||||
ARDUINO_EXCLUDED_IDF_COMPONENTS = (
|
||||
"chmorgan__esp-libhelix-mp3", # MP3 decoder - not used
|
||||
"espressif__cbor", # CBOR library - only used by RainMaker/Insights
|
||||
"espressif__esp-dsp", # DSP library - not used
|
||||
"espressif__esp-modbus", # Modbus - ESPHome has its own
|
||||
"espressif__esp-sr", # Speech recognition - not used
|
||||
"espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used
|
||||
"espressif__esp-zigbee-lib", # Zigbee library - not used
|
||||
"espressif__esp_diag_data_store", # Diagnostics - not used
|
||||
"espressif__esp_diagnostics", # Diagnostics - not used
|
||||
"espressif__esp_hosted", # ESP hosted - only for ESP32-P4
|
||||
"espressif__esp_insights", # ESP Insights - not used
|
||||
"espressif__esp_modem", # Modem library - not used
|
||||
"espressif__esp_rainmaker", # RainMaker - not used
|
||||
"espressif__esp_rcp_update", # RCP update - RainMaker transitive dep
|
||||
"espressif__esp_schedule", # Schedule - RainMaker transitive dep
|
||||
"espressif__esp_secure_cert_mgr", # Secure cert - RainMaker transitive dep
|
||||
"espressif__esp_wifi_remote", # WiFi remote - only for ESP32-P4
|
||||
"espressif__json_generator", # JSON generator - RainMaker transitive dep
|
||||
"espressif__json_parser", # JSON parser - RainMaker transitive dep
|
||||
"espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly
|
||||
"espressif__libsodium", # Crypto - ESPHome uses its own noise-c library
|
||||
"espressif__network_provisioning", # Network provisioning - not used
|
||||
"espressif__qrcode", # QR code - not used
|
||||
"espressif__rmaker_common", # RainMaker common - not used
|
||||
"joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem
|
||||
)
|
||||
|
||||
# Mapping of Arduino libraries to IDF managed components they require
|
||||
# When an Arduino library is enabled via cg.add_library(), these components
|
||||
# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS.
|
||||
#
|
||||
# Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have
|
||||
# conditional maybe_add_component() calls in arduino-esp32/CMakeLists.txt that handle
|
||||
# their managed component dependencies. Our mapping is primarily needed for libraries
|
||||
# that don't have such conditionals (Ethernet, PPP, Zigbee, RainMaker, Insights, etc.)
|
||||
# and to ensure the stubs are removed from our idf_component.yml overrides.
|
||||
ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = {
|
||||
"BLE": ("esp_driver_gptimer",),
|
||||
"BluetoothSerial": ("esp_driver_gptimer",),
|
||||
"ESP_HostedOTA": ("espressif__esp_hosted", "espressif__esp_wifi_remote"),
|
||||
"ESP_SR": ("espressif__esp-sr",),
|
||||
"Ethernet": ("espressif__lan867x",),
|
||||
"FFat": ("fatfs",),
|
||||
"Insights": (
|
||||
"espressif__cbor",
|
||||
"espressif__esp_insights",
|
||||
"espressif__esp_diagnostics",
|
||||
"espressif__esp_diag_data_store",
|
||||
"espressif__rmaker_common", # Transitive dep from esp_insights
|
||||
),
|
||||
"LittleFS": ("joltwallet__littlefs",),
|
||||
"Matter": ("espressif__esp_matter",),
|
||||
"PPP": ("espressif__esp_modem",),
|
||||
"RainMaker": (
|
||||
# Direct deps from idf_component.yml
|
||||
"espressif__cbor",
|
||||
"espressif__esp_rainmaker",
|
||||
"espressif__esp_insights",
|
||||
"espressif__esp_diagnostics",
|
||||
"espressif__esp_diag_data_store",
|
||||
"espressif__rmaker_common",
|
||||
"espressif__qrcode",
|
||||
# Transitive deps from esp_rainmaker
|
||||
"espressif__esp_rcp_update",
|
||||
"espressif__esp_schedule",
|
||||
"espressif__esp_secure_cert_mgr",
|
||||
"espressif__json_generator",
|
||||
"espressif__json_parser",
|
||||
"espressif__network_provisioning",
|
||||
),
|
||||
"SD": ("fatfs",),
|
||||
"SD_MMC": ("fatfs",),
|
||||
"SPIFFS": ("spiffs",),
|
||||
"WiFiProv": ("espressif__network_provisioning", "espressif__qrcode"),
|
||||
"Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"),
|
||||
}
|
||||
|
||||
# Arduino library to Arduino library dependencies
|
||||
# When enabling one library, also enable its dependencies
|
||||
# Kconfig "select" statements don't work with CONFIG_ARDUINO_SELECTIVE_COMPILATION
|
||||
ARDUINO_LIBRARY_DEPENDENCIES: dict[str, tuple[str, ...]] = {
|
||||
"Ethernet": ("Network",),
|
||||
"WiFi": ("Network",),
|
||||
}
|
||||
|
||||
|
||||
def _idf_component_stub_name(component: str) -> str:
|
||||
"""Get stub directory name from IDF component name.
|
||||
|
||||
Component names are typically namespace__name (e.g., espressif__cbor).
|
||||
Returns just the name part (e.g., cbor). If no namespace is present,
|
||||
returns the original component name.
|
||||
"""
|
||||
_prefix, sep, suffix = component.partition("__")
|
||||
return suffix if sep else component
|
||||
|
||||
|
||||
def _idf_component_dep_name(component: str) -> str:
|
||||
"""Convert IDF component name to dependency format.
|
||||
|
||||
Converts espressif__cbor to espressif/cbor.
|
||||
"""
|
||||
return component.replace("__", "/")
|
||||
|
||||
|
||||
# Arduino libraries to disable by default when using Arduino framework
|
||||
# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core
|
||||
# (HardwareSerial, Print, Stream, GPIO functions which are always compiled)
|
||||
# Components use cg.add_library() which auto-enables any they need
|
||||
# This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt
|
||||
ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset(
|
||||
{
|
||||
"ArduinoOTA",
|
||||
"AsyncUDP",
|
||||
"BLE",
|
||||
"BluetoothSerial",
|
||||
"DNSServer",
|
||||
"EEPROM",
|
||||
"ESP_HostedOTA",
|
||||
"ESP_I2S",
|
||||
"ESP_NOW",
|
||||
"ESP_SR",
|
||||
"ESPmDNS",
|
||||
"Ethernet",
|
||||
"FFat",
|
||||
"FS",
|
||||
"Hash",
|
||||
"HTTPClient",
|
||||
"HTTPUpdate",
|
||||
"Insights",
|
||||
"LittleFS",
|
||||
"Matter",
|
||||
"NetBIOS",
|
||||
"Network",
|
||||
"NetworkClientSecure",
|
||||
"OpenThread",
|
||||
"PPP",
|
||||
"Preferences",
|
||||
"RainMaker",
|
||||
"SD",
|
||||
"SD_MMC",
|
||||
"SimpleBLE",
|
||||
"SPI",
|
||||
"SPIFFS",
|
||||
"Ticker",
|
||||
"Update",
|
||||
"USB",
|
||||
"WebServer",
|
||||
"WiFi",
|
||||
"WiFiProv",
|
||||
"Wire",
|
||||
"Zigbee",
|
||||
}
|
||||
)
|
||||
|
||||
# ESP32 (original) chip revision options
|
||||
# Setting minimum revision to 3.0 or higher:
|
||||
# - Reduces flash size by excluding workaround code for older chip bugs
|
||||
@@ -237,7 +401,13 @@ def set_core_data(config):
|
||||
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
|
||||
# Initialize with default exclusions - components can call include_builtin_idf_component()
|
||||
# to re-enable any they need
|
||||
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
|
||||
excluded = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
|
||||
# Add Arduino-specific managed component exclusions when using Arduino framework
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
||||
excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS)
|
||||
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded
|
||||
# Initialize Arduino library tracking - cg.add_library() auto-enables libraries
|
||||
CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set()
|
||||
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
|
||||
config[CONF_FRAMEWORK][CONF_VERSION]
|
||||
)
|
||||
@@ -385,6 +555,26 @@ def include_builtin_idf_component(name: str) -> None:
|
||||
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name)
|
||||
|
||||
|
||||
def _enable_arduino_library(name: str) -> None:
|
||||
"""Enable an Arduino library that is disabled by default.
|
||||
|
||||
This is called automatically by CORE.add_library() when a component adds
|
||||
an Arduino library via cg.add_library(). Components should not call this
|
||||
directly - just use cg.add_library("LibName", None).
|
||||
|
||||
Args:
|
||||
name: The library name (e.g., "Wire", "SPI", "WiFi")
|
||||
"""
|
||||
enabled_libs: set[str] = CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES]
|
||||
enabled_libs.add(name)
|
||||
# Also enable any required Arduino library dependencies
|
||||
for dep_lib in ARDUINO_LIBRARY_DEPENDENCIES.get(name, ()):
|
||||
enabled_libs.add(dep_lib)
|
||||
# Also enable any required IDF components
|
||||
for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()):
|
||||
include_builtin_idf_component(idf_component)
|
||||
|
||||
|
||||
def add_extra_script(stage: str, filename: str, path: Path):
|
||||
"""Add an extra script to the project."""
|
||||
key = f"{stage}:{filename}"
|
||||
@@ -1126,6 +1316,27 @@ async def _write_exclude_components() -> None:
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _write_arduino_libraries_sdkconfig() -> None:
|
||||
"""Write Arduino selective compilation sdkconfig after all components have added libraries.
|
||||
|
||||
This must run at FINAL priority so that all components have had a chance to call
|
||||
cg.add_library() which auto-enables Arduino libraries via _enable_arduino_library().
|
||||
"""
|
||||
if KEY_ESP32 not in CORE.data:
|
||||
return
|
||||
# Enable Arduino selective compilation to disable unused Arduino libraries
|
||||
# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core
|
||||
# (HardwareSerial, Print, Stream, GPIO functions which are always compiled)
|
||||
# cg.add_library() auto-enables needed libraries; users can also add
|
||||
# libraries via esphome: libraries: config which calls cg.add_library()
|
||||
add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True)
|
||||
enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set())
|
||||
for lib in ARDUINO_DISABLED_LIBRARIES:
|
||||
# Enable if explicitly requested, disable otherwise
|
||||
add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _add_yaml_idf_components(components: list[ConfigType]):
|
||||
"""Add IDF components from YAML config with final priority to override code-added components."""
|
||||
@@ -1540,6 +1751,11 @@ async def to_code(config):
|
||||
# Default exclusions are added in set_core_data() during config validation.
|
||||
CORE.add_job(_write_exclude_components)
|
||||
|
||||
# Write Arduino selective compilation sdkconfig at FINAL priority after all
|
||||
# components have had a chance to call cg.add_library() to enable libraries they need.
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
||||
CORE.add_job(_write_arduino_libraries_sdkconfig)
|
||||
|
||||
|
||||
APP_PARTITION_SIZES = {
|
||||
"2MB": 0x0C0000, # 768 KB
|
||||
@@ -1620,11 +1836,49 @@ def _write_sdkconfig():
|
||||
|
||||
def _write_idf_component_yml():
|
||||
yml_path = CORE.relative_build_path("src/idf_component.yml")
|
||||
dependencies: dict[str, dict] = {}
|
||||
|
||||
# For Arduino builds, override unused managed components from the Arduino framework
|
||||
# by pointing them to empty stub directories using override_path
|
||||
# This prevents the IDF component manager from downloading the real components
|
||||
if CORE.using_arduino:
|
||||
# Determine which IDF components are needed by enabled Arduino libraries
|
||||
enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set())
|
||||
required_idf_components = {
|
||||
comp
|
||||
for lib in enabled_libs
|
||||
for comp in ARDUINO_LIBRARY_IDF_COMPONENTS.get(lib, ())
|
||||
}
|
||||
|
||||
# Only stub components that are not required by any enabled Arduino library
|
||||
components_to_stub = (
|
||||
set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components
|
||||
)
|
||||
|
||||
stubs_dir = CORE.relative_build_path("component_stubs")
|
||||
stubs_dir.mkdir(exist_ok=True)
|
||||
for component_name in components_to_stub:
|
||||
# Create stub directory with minimal CMakeLists.txt
|
||||
stub_path = stubs_dir / _idf_component_stub_name(component_name)
|
||||
stub_path.mkdir(exist_ok=True)
|
||||
stub_cmake = stub_path / "CMakeLists.txt"
|
||||
if not stub_cmake.exists():
|
||||
stub_cmake.write_text("idf_component_register()\n")
|
||||
dependencies[_idf_component_dep_name(component_name)] = {
|
||||
"version": "*",
|
||||
"override_path": str(stub_path),
|
||||
}
|
||||
|
||||
# Remove stubs for components that are now required by enabled libraries
|
||||
for component_name in required_idf_components:
|
||||
stub_path = stubs_dir / _idf_component_stub_name(component_name)
|
||||
if stub_path.exists():
|
||||
shutil.rmtree(stub_path)
|
||||
|
||||
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
|
||||
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
|
||||
dependencies = {}
|
||||
for name, component in components.items():
|
||||
dependency = {}
|
||||
dependency: dict[str, str] = {}
|
||||
if component[KEY_REF]:
|
||||
dependency["version"] = component[KEY_REF]
|
||||
if component[KEY_REPO]:
|
||||
@@ -1632,9 +1886,8 @@ def _write_idf_component_yml():
|
||||
if component[KEY_PATH]:
|
||||
dependency["path"] = component[KEY_PATH]
|
||||
dependencies[name] = dependency
|
||||
contents = yaml_util.dump({"dependencies": dependencies})
|
||||
else:
|
||||
contents = ""
|
||||
|
||||
contents = yaml_util.dump({"dependencies": dependencies}) if dependencies else ""
|
||||
if write_file_if_changed(yml_path, contents):
|
||||
dependencies_lock = CORE.relative_build_path("dependencies.lock")
|
||||
if dependencies_lock.is_file():
|
||||
|
||||
@@ -7,6 +7,7 @@ KEY_VARIANT = "variant"
|
||||
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
|
||||
KEY_COMPONENTS = "components"
|
||||
KEY_EXCLUDE_COMPONENTS = "exclude_components"
|
||||
KEY_ARDUINO_LIBRARIES = "arduino_libraries"
|
||||
KEY_REPO = "repo"
|
||||
KEY_REF = "ref"
|
||||
KEY_REFRESH = "refresh"
|
||||
|
||||
@@ -196,14 +196,11 @@ bool Esp32HostedUpdate::fetch_manifest_() {
|
||||
int read_or_error = container->read(buf, sizeof(buf));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
auto result =
|
||||
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||
// but this is defensive code in case chunked transfer encoding support is added in the future.
|
||||
if (result != http_request::HttpReadLoopResult::DATA)
|
||||
break; // COMPLETE, ERROR, or TIMEOUT
|
||||
break; // ERROR or TIMEOUT
|
||||
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
|
||||
}
|
||||
container->end();
|
||||
@@ -324,14 +321,9 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result =
|
||||
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||
// but this is defensive code in case chunked transfer encoding support is added in the future.
|
||||
if (result == http_request::HttpReadLoopResult::COMPLETE)
|
||||
break;
|
||||
if (result != http_request::HttpReadLoopResult::DATA) {
|
||||
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading firmware data");
|
||||
|
||||
@@ -13,7 +13,7 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WIFI,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.core import HexInt
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
@@ -124,9 +124,6 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
# ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks
|
||||
# This enables low-latency event processing instead of waiting for select() timeout
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
@@ -427,9 +427,6 @@ async def to_code(config):
|
||||
# Add LAN867x 10BASE-T1S PHY support component
|
||||
add_idf_component(name="espressif/lan867x", ref="2.0.0")
|
||||
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
CORE.add_job(final_step)
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ struct Header {
|
||||
enum HttpStatus {
|
||||
HTTP_STATUS_OK = 200,
|
||||
HTTP_STATUS_NO_CONTENT = 204,
|
||||
HTTP_STATUS_RESET_CONTENT = 205,
|
||||
HTTP_STATUS_PARTIAL_CONTENT = 206,
|
||||
|
||||
/* 3xx - Redirection */
|
||||
@@ -127,21 +126,19 @@ struct HttpReadResult {
|
||||
|
||||
/// Result of processing a non-blocking read with timeout (for manual loops)
|
||||
enum class HttpReadLoopResult : uint8_t {
|
||||
DATA, ///< Data was read, process it
|
||||
COMPLETE, ///< All content has been read, caller should exit loop
|
||||
RETRY, ///< No data yet, already delayed, caller should continue loop
|
||||
ERROR, ///< Read error, caller should exit loop
|
||||
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
||||
DATA, ///< Data was read, process it
|
||||
RETRY, ///< No data yet, already delayed, caller should continue loop
|
||||
ERROR, ///< Read error, caller should exit loop
|
||||
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
||||
};
|
||||
|
||||
/// Process a read result with timeout tracking and delay handling
|
||||
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
|
||||
/// @param last_data_time Time of last successful read, updated when data received
|
||||
/// @param timeout_ms Maximum time to wait for data
|
||||
/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete())
|
||||
/// @return How the caller should proceed - see HttpReadLoopResult enum
|
||||
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms,
|
||||
bool is_read_complete) {
|
||||
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
|
||||
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
|
||||
uint32_t timeout_ms) {
|
||||
if (bytes_read_or_error > 0) {
|
||||
last_data_time = millis();
|
||||
return HttpReadLoopResult::DATA;
|
||||
@@ -149,10 +146,7 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_
|
||||
if (bytes_read_or_error < 0) {
|
||||
return HttpReadLoopResult::ERROR;
|
||||
}
|
||||
// bytes_read_or_error == 0: either "no data yet" or "all content read"
|
||||
if (is_read_complete) {
|
||||
return HttpReadLoopResult::COMPLETE;
|
||||
}
|
||||
// bytes_read_or_error == 0: no data available yet
|
||||
if (millis() - last_data_time >= timeout_ms) {
|
||||
return HttpReadLoopResult::TIMEOUT;
|
||||
}
|
||||
@@ -165,9 +159,9 @@ class HttpRequestComponent;
|
||||
class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
public:
|
||||
virtual ~HttpContainer() = default;
|
||||
size_t content_length{0};
|
||||
int status_code{-1}; ///< -1 indicates no response received yet
|
||||
uint32_t duration_ms{0};
|
||||
size_t content_length;
|
||||
int status_code;
|
||||
uint32_t duration_ms;
|
||||
|
||||
/**
|
||||
* @brief Read data from the HTTP response body.
|
||||
@@ -200,24 +194,9 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
virtual void end() = 0;
|
||||
|
||||
void set_secure(bool secure) { this->secure_ = secure; }
|
||||
void set_chunked(bool chunked) { this->is_chunked_ = chunked; }
|
||||
|
||||
size_t get_bytes_read() const { return this->bytes_read_; }
|
||||
|
||||
/// Check if all expected content has been read
|
||||
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
|
||||
bool is_read_complete() const {
|
||||
// Per RFC 9112, these responses have no body:
|
||||
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
|
||||
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
|
||||
this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) {
|
||||
return true;
|
||||
}
|
||||
// For non-chunked responses, complete when bytes_read >= content_length
|
||||
// This handles both Content-Length: 0 and Content-Length: N cases
|
||||
return !this->is_chunked_ && this->bytes_read_ >= this->content_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get response headers.
|
||||
*
|
||||
@@ -230,7 +209,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
protected:
|
||||
size_t bytes_read_{0};
|
||||
bool secure_{false};
|
||||
bool is_chunked_{false}; ///< True if response uses chunked transfer encoding
|
||||
std::map<std::string, std::list<std::string>> response_headers_{};
|
||||
};
|
||||
|
||||
@@ -241,7 +219,7 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
/// @param total_size Total bytes to read
|
||||
/// @param chunk_size Maximum bytes per read call
|
||||
/// @param timeout_ms Read timeout in milliseconds
|
||||
/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read
|
||||
/// @return HttpReadResult with status and error_code on failure
|
||||
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
|
||||
uint32_t timeout_ms) {
|
||||
size_t read_index = 0;
|
||||
@@ -253,11 +231,9 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer,
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete());
|
||||
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result == HttpReadLoopResult::COMPLETE)
|
||||
break; // Server sent less data than requested, but transfer is complete
|
||||
if (result == HttpReadLoopResult::ERROR)
|
||||
return {HttpReadStatus::ERROR, read_bytes_or_error};
|
||||
if (result == HttpReadLoopResult::TIMEOUT)
|
||||
@@ -417,12 +393,11 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
auto result =
|
||||
http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA)
|
||||
break; // COMPLETE, ERROR, or TIMEOUT
|
||||
break; // ERROR or TIMEOUT
|
||||
read_index += read_or_error;
|
||||
}
|
||||
response_body.reserve(read_index);
|
||||
|
||||
@@ -135,23 +135,9 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
|
||||
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
|
||||
// early return check (bytes_read_ >= content_length) will never trigger.
|
||||
//
|
||||
// TODO: Chunked transfer encoding is NOT properly supported on Arduino.
|
||||
// The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where
|
||||
// esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr()
|
||||
// returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead
|
||||
// of decoded content. This wasn't noticed because requests would complete and payloads
|
||||
// were only examined on IDF. The long transfer times were also masked by the misleading
|
||||
// "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues:
|
||||
// 1. Response body is corrupted - contains chunk size headers mixed with data
|
||||
// 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout
|
||||
// The proper fix would be to use getString() for chunked responses, which decodes chunks
|
||||
// internally, but this buffers the entire response in memory.
|
||||
int content_length = container->client_.getSize();
|
||||
ESP_LOGD(TAG, "Content-Length: %d", content_length);
|
||||
container->content_length = (size_t) content_length;
|
||||
// -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding
|
||||
container->set_chunked(content_length == -1);
|
||||
container->duration_ms = millis() - start;
|
||||
|
||||
return container;
|
||||
@@ -192,9 +178,9 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
// Check if we've read all expected content (non-chunked only)
|
||||
// For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false
|
||||
if (this->is_read_complete()) {
|
||||
// Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX)
|
||||
// For chunked encoding (content_length == SIZE_MAX), we can't use this check
|
||||
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
// No data available - check if connection is still open
|
||||
|
||||
@@ -160,7 +160,6 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
|
||||
// The read() method handles content_length == 0 specially to support chunked responses.
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
container->feed_wdt();
|
||||
@@ -196,7 +195,6 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
|
||||
container->feed_wdt();
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
container->feed_wdt();
|
||||
@@ -241,9 +239,10 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
// Check if we've already read all expected content (non-chunked only)
|
||||
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
|
||||
if (this->is_read_complete()) {
|
||||
// Check if we've already read all expected content
|
||||
// Skip this check when content_length is 0 (chunked transfer encoding or unknown length)
|
||||
// For chunked responses, esp_http_client_read() will return 0 when all data is received
|
||||
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
|
||||
|
||||
@@ -130,13 +130,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||
// but this is defensive code in case chunked transfer encoding support is added for OTA in the future.
|
||||
if (result == HttpReadLoopResult::COMPLETE)
|
||||
break;
|
||||
if (result != HttpReadLoopResult::DATA) {
|
||||
if (result == HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading data");
|
||||
|
||||
@@ -114,6 +114,7 @@ async def to_code(config):
|
||||
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
|
||||
cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb"))
|
||||
|
||||
cg.add_library("WiFi", None)
|
||||
cg.add_library("NetworkClientSecure", None)
|
||||
cg.add_library("HTTPClient", None)
|
||||
cg.add_library("esphome/ESP32-audioI2S", "2.3.0")
|
||||
|
||||
@@ -30,7 +30,7 @@ from esphome.const import (
|
||||
UNIT_PERCENT,
|
||||
UNIT_WATT,
|
||||
)
|
||||
from esphome.core import coroutine
|
||||
from esphome.core import CORE, coroutine
|
||||
|
||||
CODEOWNERS = ["@dudanov"]
|
||||
DEPENDENCIES = ["climate", "uart"]
|
||||
@@ -290,4 +290,7 @@ async def to_code(config):
|
||||
if CONF_HUMIDITY_SETPOINT in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
|
||||
cg.add(var.set_humidity_setpoint_sensor(sens))
|
||||
# MideaUART library requires WiFi (WiFi auto-enables Network via dependency mapping)
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("WiFi", None)
|
||||
cg.add_library("dudanov/MideaUART", "1.1.9")
|
||||
|
||||
@@ -643,34 +643,10 @@ static bool topic_match(const char *message, const char *subscription) {
|
||||
}
|
||||
|
||||
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
|
||||
#ifdef USE_ESP8266
|
||||
// IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266.
|
||||
//
|
||||
// On ESP8266, this callback is invoked directly from the lwIP/AsyncTCP network stack
|
||||
// which runs in the "sys" context with a very limited stack (~4KB). By the time we
|
||||
// reach this function, the stack is already partially consumed by the network
|
||||
// processing chain: tcp_input -> AsyncClient::_recv -> AsyncMqttClient::_onMessage -> here.
|
||||
//
|
||||
// MQTT subscription callbacks can trigger arbitrary user actions (automations, HTTP
|
||||
// requests, sensor updates, etc.) which may have deep call stacks of their own.
|
||||
// For example, an HTTP request action requires: DNS lookup -> TCP connect -> TLS
|
||||
// handshake (if HTTPS) -> request formatting. This easily overflows the remaining
|
||||
// system stack space, causing a LoadStoreAlignmentCause exception or silent corruption.
|
||||
//
|
||||
// By deferring to the main loop, we ensure callbacks execute with a fresh, full-size
|
||||
// stack in the normal application context rather than the constrained network task.
|
||||
//
|
||||
// DO NOT REMOVE THIS DEFER without understanding the above. It may appear to work
|
||||
// in simple tests but will cause crashes with complex automations.
|
||||
this->defer([this, topic, payload]() {
|
||||
#endif
|
||||
for (auto &subscription : this->subscriptions_) {
|
||||
if (topic_match(topic.c_str(), subscription.topic.c_str()))
|
||||
subscription.callback(topic, payload);
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
});
|
||||
#endif
|
||||
for (auto &subscription : this->subscriptions_) {
|
||||
if (topic_match(topic.c_str(), subscription.topic.c_str()))
|
||||
subscription.callback(topic, payload);
|
||||
}
|
||||
}
|
||||
|
||||
// Setters
|
||||
|
||||
@@ -137,8 +137,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
@coroutine_with_priority(CoroPriority.NETWORK)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_NETWORK")
|
||||
if CORE.using_arduino and CORE.is_esp32:
|
||||
cg.add_library("Networking", None)
|
||||
# ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed
|
||||
|
||||
# Apply high performance networking settings
|
||||
# Config can explicitly enable/disable, or default to component-driven behavior
|
||||
|
||||
@@ -2,20 +2,21 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::pmsx003 {
|
||||
namespace esphome {
|
||||
namespace pmsx003 {
|
||||
|
||||
static const char *const TAG = "pmsx003";
|
||||
|
||||
static const uint8_t START_CHARACTER_1 = 0x42;
|
||||
static const uint8_t START_CHARACTER_2 = 0x4D;
|
||||
|
||||
static const uint16_t STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms
|
||||
static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms
|
||||
|
||||
static const uint16_t CMD_MEASUREMENT_MODE_PASSIVE =
|
||||
0x0000; // use `Command::MANUAL_MEASUREMENT` to trigger a measurement
|
||||
static const uint16_t CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements
|
||||
static const uint16_t CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode
|
||||
static const uint16_t CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode
|
||||
static const uint16_t PMS_CMD_MEASUREMENT_MODE_PASSIVE =
|
||||
0x0000; // use `PMS_CMD_MANUAL_MEASUREMENT` to trigger a measurement
|
||||
static const uint16_t PMS_CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements
|
||||
static const uint16_t PMS_CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode
|
||||
static const uint16_t PMS_CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode
|
||||
|
||||
void PMSX003Component::setup() {}
|
||||
|
||||
@@ -41,7 +42,7 @@ void PMSX003Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
|
||||
if (this->update_interval_ <= STABILISING_MS) {
|
||||
if (this->update_interval_ <= PMS_STABILISING_MS) {
|
||||
ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)");
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
|
||||
@@ -54,44 +55,44 @@ void PMSX003Component::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Initialize sensor mode on first loop
|
||||
if (!this->initialised_) {
|
||||
if (this->update_interval_ > STABILISING_MS) {
|
||||
if (this->initialised_ == 0) {
|
||||
if (this->update_interval_ > PMS_STABILISING_MS) {
|
||||
// Long update interval: use passive mode with sleep/wake cycles
|
||||
this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_PASSIVE);
|
||||
this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP);
|
||||
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE);
|
||||
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP);
|
||||
} else {
|
||||
// Short/zero update interval: use active continuous mode
|
||||
this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_ACTIVE);
|
||||
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_ACTIVE);
|
||||
}
|
||||
this->initialised_ = true;
|
||||
this->initialised_ = 1;
|
||||
}
|
||||
|
||||
// If we update less often than it takes the device to stabilise, spin the fan down
|
||||
// rather than running it constantly. It does take some time to stabilise, so we
|
||||
// need to keep track of what state we're in.
|
||||
if (this->update_interval_ > STABILISING_MS) {
|
||||
if (this->update_interval_ > PMS_STABILISING_MS) {
|
||||
switch (this->state_) {
|
||||
case State::IDLE:
|
||||
case PMSX003_STATE_IDLE:
|
||||
// Power on the sensor now so it'll be ready when we hit the update time
|
||||
if (now - this->last_update_ < (this->update_interval_ - STABILISING_MS))
|
||||
if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS))
|
||||
return;
|
||||
|
||||
this->state_ = State::STABILISING;
|
||||
this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP);
|
||||
this->state_ = PMSX003_STATE_STABILISING;
|
||||
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP);
|
||||
this->fan_on_time_ = now;
|
||||
return;
|
||||
case State::STABILISING:
|
||||
case PMSX003_STATE_STABILISING:
|
||||
// wait for the sensor to be stable
|
||||
if (now - this->fan_on_time_ < STABILISING_MS)
|
||||
if (now - this->fan_on_time_ < PMS_STABILISING_MS)
|
||||
return;
|
||||
// consume any command responses that are in the serial buffer
|
||||
while (this->available())
|
||||
this->read_byte(&this->data_[0]);
|
||||
// Trigger a new read
|
||||
this->send_command_(Command::MANUAL_MEASUREMENT, 0);
|
||||
this->state_ = State::WAITING;
|
||||
this->send_command_(PMS_CMD_MANUAL_MEASUREMENT, 0);
|
||||
this->state_ = PMSX003_STATE_WAITING;
|
||||
break;
|
||||
case State::WAITING:
|
||||
case PMSX003_STATE_WAITING:
|
||||
// Just go ahead and read stuff
|
||||
break;
|
||||
}
|
||||
@@ -179,28 +180,27 @@ optional<bool> PMSX003Component::check_byte_() {
|
||||
}
|
||||
|
||||
bool PMSX003Component::check_payload_length_(uint16_t payload_length) {
|
||||
// https://avaldebe.github.io/PyPMS/sensors/Plantower/
|
||||
switch (this->type_) {
|
||||
case Type::PMSX003:
|
||||
case PMSX003_TYPE_X003:
|
||||
// The expected payload length is typically 28 bytes.
|
||||
// However, a 20-byte payload check was already present in the code.
|
||||
// No official documentation was found confirming this.
|
||||
// Retaining this check to avoid breaking existing behavior.
|
||||
return payload_length == 28 || payload_length == 20; // 2*13+2
|
||||
case Type::PMS5003S:
|
||||
case Type::PMS5003T:
|
||||
case PMSX003_TYPE_5003T:
|
||||
case PMSX003_TYPE_5003S:
|
||||
return payload_length == 28; // 2*13+2 (Data 13 not set/reserved)
|
||||
case Type::PMS5003ST:
|
||||
case PMSX003_TYPE_5003ST:
|
||||
return payload_length == 36; // 2*17+2 (Data 16 not set/reserved)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PMSX003Component::send_command_(Command cmd, uint16_t data) {
|
||||
void PMSX003Component::send_command_(PMSX0003Command cmd, uint16_t data) {
|
||||
uint8_t send_data[7] = {
|
||||
START_CHARACTER_1, // Start Byte 1
|
||||
START_CHARACTER_2, // Start Byte 2
|
||||
static_cast<uint8_t>(cmd), // Command
|
||||
cmd, // Command
|
||||
uint8_t((data >> 8) & 0xFF), // Data 1
|
||||
uint8_t((data >> 0) & 0xFF), // Data 2
|
||||
0, // Verify Byte 1
|
||||
@@ -265,7 +265,7 @@ void PMSX003Component::parse_data_() {
|
||||
if (this->pm_particles_25um_sensor_ != nullptr)
|
||||
this->pm_particles_25um_sensor_->publish_state(pm_particles_25um);
|
||||
|
||||
if (this->type_ == Type::PMS5003T) {
|
||||
if (this->type_ == PMSX003_TYPE_5003T) {
|
||||
ESP_LOGD(TAG,
|
||||
"Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, "
|
||||
"PM2.5 Particles %u Count/0.1L",
|
||||
@@ -289,7 +289,7 @@ void PMSX003Component::parse_data_() {
|
||||
}
|
||||
|
||||
// Formaldehyde
|
||||
if (this->type_ == Type::PMS5003S || this->type_ == Type::PMS5003ST) {
|
||||
if (this->type_ == PMSX003_TYPE_5003ST || this->type_ == PMSX003_TYPE_5003S) {
|
||||
const uint16_t formaldehyde = this->get_16_bit_uint_(28);
|
||||
|
||||
ESP_LOGD(TAG, "Got Formaldehyde: %u µg/m^3", formaldehyde);
|
||||
@@ -299,8 +299,8 @@ void PMSX003Component::parse_data_() {
|
||||
}
|
||||
|
||||
// Temperature and Humidity
|
||||
if (this->type_ == Type::PMS5003T || this->type_ == Type::PMS5003ST) {
|
||||
const uint8_t temperature_offset = (this->type_ == Type::PMS5003T) ? 24 : 30;
|
||||
if (this->type_ == PMSX003_TYPE_5003ST || this->type_ == PMSX003_TYPE_5003T) {
|
||||
const uint8_t temperature_offset = (this->type_ == PMSX003_TYPE_5003T) ? 24 : 30;
|
||||
|
||||
const float temperature = static_cast<int16_t>(this->get_16_bit_uint_(temperature_offset)) / 10.0f;
|
||||
const float humidity = this->get_16_bit_uint_(temperature_offset + 2) / 10.0f;
|
||||
@@ -314,7 +314,7 @@ void PMSX003Component::parse_data_() {
|
||||
}
|
||||
|
||||
// Firmware Version and Error Code
|
||||
if (this->type_ == Type::PMS5003ST) {
|
||||
if (this->type_ == PMSX003_TYPE_5003ST) {
|
||||
const uint8_t firmware_version = this->data_[36];
|
||||
const uint8_t error_code = this->data_[37];
|
||||
|
||||
@@ -323,12 +323,13 @@ void PMSX003Component::parse_data_() {
|
||||
|
||||
// Spin down the sensor again if we aren't going to need it until more time has
|
||||
// passed than it takes to stabilise
|
||||
if (this->update_interval_ > STABILISING_MS) {
|
||||
this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_SLEEP);
|
||||
this->state_ = State::IDLE;
|
||||
if (this->update_interval_ > PMS_STABILISING_MS) {
|
||||
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_SLEEP);
|
||||
this->state_ = PMSX003_STATE_IDLE;
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
} // namespace esphome::pmsx003
|
||||
} // namespace pmsx003
|
||||
} // namespace esphome
|
||||
|
||||
@@ -5,25 +5,27 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome::pmsx003 {
|
||||
namespace esphome {
|
||||
namespace pmsx003 {
|
||||
|
||||
enum class Type : uint8_t {
|
||||
PMSX003 = 0,
|
||||
PMS5003S,
|
||||
PMS5003T,
|
||||
PMS5003ST,
|
||||
enum PMSX0003Command : uint8_t {
|
||||
PMS_CMD_MEASUREMENT_MODE =
|
||||
0xE1, // Data Options: `PMS_CMD_MEASUREMENT_MODE_PASSIVE`, `PMS_CMD_MEASUREMENT_MODE_ACTIVE`
|
||||
PMS_CMD_MANUAL_MEASUREMENT = 0xE2,
|
||||
PMS_CMD_SLEEP_MODE = 0xE4, // Data Options: `PMS_CMD_SLEEP_MODE_SLEEP`, `PMS_CMD_SLEEP_MODE_WAKEUP`
|
||||
};
|
||||
|
||||
enum class Command : uint8_t {
|
||||
MEASUREMENT_MODE = 0xE1, // Data Options: `CMD_MEASUREMENT_MODE_PASSIVE`, `CMD_MEASUREMENT_MODE_ACTIVE`
|
||||
MANUAL_MEASUREMENT = 0xE2,
|
||||
SLEEP_MODE = 0xE4, // Data Options: `CMD_SLEEP_MODE_SLEEP`, `CMD_SLEEP_MODE_WAKEUP`
|
||||
enum PMSX003Type {
|
||||
PMSX003_TYPE_X003 = 0,
|
||||
PMSX003_TYPE_5003T,
|
||||
PMSX003_TYPE_5003ST,
|
||||
PMSX003_TYPE_5003S,
|
||||
};
|
||||
|
||||
enum class State : uint8_t {
|
||||
IDLE = 0,
|
||||
STABILISING,
|
||||
WAITING,
|
||||
enum PMSX003State {
|
||||
PMSX003_STATE_IDLE = 0,
|
||||
PMSX003_STATE_STABILISING,
|
||||
PMSX003_STATE_WAITING,
|
||||
};
|
||||
|
||||
class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
@@ -35,7 +37,7 @@ class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
|
||||
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
|
||||
|
||||
void set_type(Type type) { this->type_ = type; }
|
||||
void set_type(PMSX003Type type) { this->type_ = type; }
|
||||
|
||||
void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor) { this->pm_1_0_std_sensor_ = pm_1_0_std_sensor; }
|
||||
void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor) { this->pm_2_5_std_sensor_ = pm_2_5_std_sensor; }
|
||||
@@ -75,20 +77,20 @@ class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
optional<bool> check_byte_();
|
||||
void parse_data_();
|
||||
bool check_payload_length_(uint16_t payload_length);
|
||||
void send_command_(Command cmd, uint16_t data);
|
||||
void send_command_(PMSX0003Command cmd, uint16_t data);
|
||||
uint16_t get_16_bit_uint_(uint8_t start_index) const {
|
||||
return encode_uint16(this->data_[start_index], this->data_[start_index + 1]);
|
||||
}
|
||||
|
||||
Type type_;
|
||||
State state_{State::IDLE};
|
||||
bool initialised_{false};
|
||||
uint8_t data_[64];
|
||||
uint8_t data_index_{0};
|
||||
uint8_t initialised_{0};
|
||||
uint32_t fan_on_time_{0};
|
||||
uint32_t last_update_{0};
|
||||
uint32_t last_transmission_{0};
|
||||
uint32_t update_interval_{0};
|
||||
PMSX003State state_{PMSX003_STATE_IDLE};
|
||||
PMSX003Type type_;
|
||||
|
||||
// "Standard Particle"
|
||||
sensor::Sensor *pm_1_0_std_sensor_{nullptr};
|
||||
@@ -116,4 +118,5 @@ class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::pmsx003
|
||||
} // namespace pmsx003
|
||||
} // namespace esphome
|
||||
|
||||
@@ -41,33 +41,33 @@ PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Com
|
||||
PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor)
|
||||
|
||||
TYPE_PMSX003 = "PMSX003"
|
||||
TYPE_PMS5003S = "PMS5003S"
|
||||
TYPE_PMS5003T = "PMS5003T"
|
||||
TYPE_PMS5003ST = "PMS5003ST"
|
||||
TYPE_PMS5003S = "PMS5003S"
|
||||
|
||||
Type = pmsx003_ns.enum("Type", is_class=True)
|
||||
PMSX003Type = pmsx003_ns.enum("PMSX003Type")
|
||||
|
||||
PMSX003_TYPES = {
|
||||
TYPE_PMSX003: Type.PMSX003,
|
||||
TYPE_PMS5003S: Type.PMS5003S,
|
||||
TYPE_PMS5003T: Type.PMS5003T,
|
||||
TYPE_PMS5003ST: Type.PMS5003ST,
|
||||
TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
|
||||
TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
|
||||
TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST,
|
||||
TYPE_PMS5003S: PMSX003Type.PMSX003_TYPE_5003S,
|
||||
}
|
||||
|
||||
SENSORS_TO_TYPE = {
|
||||
CONF_PM_1_0_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_2_5_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_10_0_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_0_3UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_0_5UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_1_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_2_5UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_5_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003ST],
|
||||
CONF_PM_10_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003ST],
|
||||
CONF_FORMALDEHYDE: [TYPE_PMS5003S, TYPE_PMS5003ST],
|
||||
CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_1_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_2_5_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_10_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_0_3UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_0_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_1_0UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_2_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_5_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_PM_10_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_FORMALDEHYDE: [TYPE_PMS5003ST, TYPE_PMS5003S],
|
||||
CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
}
|
||||
|
||||
@@ -38,11 +38,8 @@ async def to_code(config):
|
||||
cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1)
|
||||
return
|
||||
|
||||
# ESP32 uses IDF web server (early return above), so this is for other Arduino platforms
|
||||
if CORE.using_arduino:
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("WiFi", None)
|
||||
cg.add_library("FS", None)
|
||||
cg.add_library("Update", None)
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266WiFi", None)
|
||||
if CORE.is_libretiny:
|
||||
@@ -53,4 +50,4 @@ async def to_code(config):
|
||||
"lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"]
|
||||
)
|
||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.5")
|
||||
|
||||
@@ -3,6 +3,7 @@ from esphome.components.light.effects import register_addressable_effect
|
||||
from esphome.components.light.types import AddressableLightEffect
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_NAME, CONF_PORT
|
||||
from esphome.core import CORE
|
||||
|
||||
wled_ns = cg.esphome_ns.namespace("wled")
|
||||
WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect)
|
||||
@@ -27,4 +28,6 @@ async def wled_light_effect_to_code(config, effect_id):
|
||||
cg.add(effect.set_port(config[CONF_PORT]))
|
||||
cg.add(effect.set_sync_group_mask(config[CONF_SYNC_GROUP_MASK]))
|
||||
cg.add(effect.set_blank_on_start(config[CONF_BLANK_ON_START]))
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("WiFi", None)
|
||||
return effect
|
||||
|
||||
@@ -888,6 +888,16 @@ class EsphomeCore:
|
||||
library.name if "/" not in library.name else library.name.split("/")[-1]
|
||||
)
|
||||
|
||||
# Auto-enable Arduino libraries on ESP32 Arduino builds
|
||||
if self.is_esp32 and self.using_arduino:
|
||||
from esphome.components.esp32 import (
|
||||
ARDUINO_DISABLED_LIBRARIES,
|
||||
_enable_arduino_library,
|
||||
)
|
||||
|
||||
if short_name in ARDUINO_DISABLED_LIBRARIES:
|
||||
_enable_arduino_library(short_name)
|
||||
|
||||
if short_name not in self.platformio_libraries:
|
||||
_LOGGER.debug("Adding library: %s", library)
|
||||
self.platformio_libraries[short_name] = library
|
||||
|
||||
@@ -210,7 +210,7 @@ void Application::loop() {
|
||||
#ifdef USE_ESP32
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100,
|
||||
ESP_LOGI(TAG, "ESP32 Chip: %s r%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100,
|
||||
chip_info.revision % 100, chip_info.cores);
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET)
|
||||
// Suggest optimization for chips that don't need the PSRAM cache workaround
|
||||
|
||||
@@ -114,7 +114,7 @@ lib_deps =
|
||||
ESP8266WiFi ; wifi (Arduino built-in)
|
||||
Update ; ota (Arduino built-in)
|
||||
ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp
|
||||
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
||||
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
|
||||
makuna/NeoPixelBus@2.7.3 ; neopixelbus
|
||||
ESP8266HTTPClient ; http_request (Arduino built-in)
|
||||
ESP8266mDNS ; mdns (Arduino built-in)
|
||||
@@ -202,7 +202,7 @@ lib_deps =
|
||||
${common:arduino.lib_deps}
|
||||
ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
||||
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
-DUSE_RP2040
|
||||
@@ -218,7 +218,7 @@ framework = arduino
|
||||
lib_compat_mode = soft
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
||||
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
|
||||
droscy/esp_wireguard@0.4.2 ; wireguard
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
|
||||
16
tests/components/i2s_audio/test.esp32-ard.yaml
Normal file
16
tests/components/i2s_audio/test.esp32-ard.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
substitutions:
|
||||
i2s_bclk_pin: GPIO15
|
||||
i2s_lrclk_pin: GPIO4
|
||||
i2s_mclk_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: test1234
|
||||
|
||||
media_player:
|
||||
- platform: i2s_audio
|
||||
name: Test Media Player
|
||||
dac_type: internal
|
||||
mode: stereo
|
||||
@@ -8,11 +8,11 @@ sensor:
|
||||
pm_10_0:
|
||||
name: PM 10.0 Concentration
|
||||
pm_1_0_std:
|
||||
name: PM 1.0 Standard Atmospheric Concentration
|
||||
name: PM 1.0 Standard Atmospher Concentration
|
||||
pm_2_5_std:
|
||||
name: PM 2.5 Standard Atmospheric Concentration
|
||||
name: PM 2.5 Standard Atmospher Concentration
|
||||
pm_10_0_std:
|
||||
name: PM 10.0 Standard Atmospheric Concentration
|
||||
name: PM 10.0 Standard Atmospher Concentration
|
||||
pm_0_3um:
|
||||
name: Particulate Count >0.3um
|
||||
pm_0_5um:
|
||||
|
||||
@@ -780,3 +780,78 @@ class TestEsphomeCore:
|
||||
target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}}
|
||||
|
||||
assert target.has_networking is False
|
||||
|
||||
def test_add_library__esp32_arduino_enables_disabled_library(self, target):
|
||||
"""Test add_library auto-enables Arduino libraries on ESP32 Arduino builds."""
|
||||
target.data[const.KEY_CORE] = {
|
||||
const.KEY_TARGET_PLATFORM: "esp32",
|
||||
const.KEY_TARGET_FRAMEWORK: "arduino",
|
||||
}
|
||||
|
||||
library = core.Library("WiFi", None)
|
||||
|
||||
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
|
||||
target.add_library(library)
|
||||
mock_enable.assert_called_once_with("WiFi")
|
||||
|
||||
assert "WiFi" in target.platformio_libraries
|
||||
|
||||
def test_add_library__esp32_arduino_ignores_non_arduino_library(self, target):
|
||||
"""Test add_library doesn't enable libraries not in ARDUINO_DISABLED_LIBRARIES."""
|
||||
target.data[const.KEY_CORE] = {
|
||||
const.KEY_TARGET_PLATFORM: "esp32",
|
||||
const.KEY_TARGET_FRAMEWORK: "arduino",
|
||||
}
|
||||
|
||||
library = core.Library("SomeOtherLib", "1.0.0")
|
||||
|
||||
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
|
||||
target.add_library(library)
|
||||
mock_enable.assert_not_called()
|
||||
|
||||
assert "SomeOtherLib" in target.platformio_libraries
|
||||
|
||||
def test_add_library__esp32_idf_does_not_enable_arduino_library(self, target):
|
||||
"""Test add_library doesn't auto-enable Arduino libraries on ESP32 IDF builds."""
|
||||
target.data[const.KEY_CORE] = {
|
||||
const.KEY_TARGET_PLATFORM: "esp32",
|
||||
const.KEY_TARGET_FRAMEWORK: "esp-idf",
|
||||
}
|
||||
|
||||
library = core.Library("WiFi", None)
|
||||
|
||||
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
|
||||
target.add_library(library)
|
||||
mock_enable.assert_not_called()
|
||||
|
||||
assert "WiFi" in target.platformio_libraries
|
||||
|
||||
def test_add_library__esp8266_does_not_enable_arduino_library(self, target):
|
||||
"""Test add_library doesn't auto-enable Arduino libraries on ESP8266."""
|
||||
target.data[const.KEY_CORE] = {
|
||||
const.KEY_TARGET_PLATFORM: "esp8266",
|
||||
const.KEY_TARGET_FRAMEWORK: "arduino",
|
||||
}
|
||||
|
||||
library = core.Library("WiFi", None)
|
||||
|
||||
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
|
||||
target.add_library(library)
|
||||
mock_enable.assert_not_called()
|
||||
|
||||
assert "WiFi" in target.platformio_libraries
|
||||
|
||||
def test_add_library__extracts_short_name_from_path(self, target):
|
||||
"""Test add_library extracts short name from library paths like owner/lib."""
|
||||
target.data[const.KEY_CORE] = {
|
||||
const.KEY_TARGET_PLATFORM: "esp32",
|
||||
const.KEY_TARGET_FRAMEWORK: "arduino",
|
||||
}
|
||||
|
||||
library = core.Library("arduino/Wire", None)
|
||||
|
||||
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
|
||||
target.add_library(library)
|
||||
mock_enable.assert_called_once_with("Wire")
|
||||
|
||||
assert "Wire" in target.platformio_libraries
|
||||
|
||||
Reference in New Issue
Block a user