mirror of
https://github.com/esphome/esphome.git
synced 2026-09-05 04:26:02 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48605d24ef | ||
|
|
b186b6d6bb | ||
|
|
e4a61c240d | ||
|
|
6573b57125 | ||
|
|
2ab3d39522 | ||
|
|
37180f748d |
@@ -112,12 +112,6 @@ jobs:
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
validate-only-components: ${{ steps.determine.outputs.validate-only-components }}
|
||||
benchmarks: ${{ steps.determine.outputs.benchmarks }}
|
||||
# "true" when this run is a pull request into one of the release
|
||||
# branches. Those pull requests are batches of changes already tested on
|
||||
# their original dev pull requests, so several jobs below trade coverage
|
||||
# for turnaround time on them. Matched exactly, not by prefix, so an
|
||||
# ordinary branch named e.g. "release-notes" is not caught by it.
|
||||
release-pr: ${{ github.base_ref == 'beta' || github.base_ref == 'release' }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -236,7 +230,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.release-pr == 'false' && needs.determine-jobs.outputs.core-ci == 'true'
|
||||
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -455,21 +449,10 @@ jobs:
|
||||
if: >-
|
||||
github.repository == 'esphome/esphome' && (
|
||||
(github.event_name == 'push' && github.ref_name == 'dev') ||
|
||||
(
|
||||
github.event_name == 'pull_request' &&
|
||||
needs.determine-jobs.outputs.release-pr == 'false' &&
|
||||
needs.determine-jobs.outputs.benchmarks == 'true'
|
||||
)
|
||||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
|
||||
)
|
||||
# CodSpeed benchmarks require a CodSpeed account linked to the repository to run
|
||||
# (https://codspeed.io) -- disabled on forks that aren't esphome/esphome itself.
|
||||
#
|
||||
# Pull requests into beta and release are skipped as well. CodSpeed compares a
|
||||
# pull request against the newest commit of its base branch that has a benchmark
|
||||
# run of its own, and only dev is benchmarked. A release pull request therefore
|
||||
# falls back to dev's latest run, so every speed-up merged into dev since the
|
||||
# release branched is reported as a regression in the release. The changes there
|
||||
# have already been benchmarked on their original dev pull requests.
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -946,7 +929,7 @@ jobs:
|
||||
ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: ${{ needs.determine-jobs.outputs.release-pr == 'true' && 32 || 16 }}
|
||||
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 32 || 16 }}
|
||||
matrix:
|
||||
batch: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
|
||||
steps:
|
||||
@@ -1033,7 +1016,7 @@ jobs:
|
||||
# - This catches pin conflicts and other issues in directly changed code
|
||||
# - Grouped tests use --testing-mode to allow config merging (disables some checks)
|
||||
# - Dependencies are safe to group since they weren't modified in this PR
|
||||
if [[ "${{ needs.determine-jobs.outputs.release-pr }}" == "true" ]]; then
|
||||
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
|
||||
directly_changed_csv=""
|
||||
echo "Testing components: $components_csv"
|
||||
echo "Target branch: ${{ github.base_ref }} - grouping all components"
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -476,7 +476,6 @@ esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/serial_proxy/* @kbx81
|
||||
esphome/components/sfa30/* @ghsensdev
|
||||
esphome/components/sfa40/* @NoQuarrel
|
||||
esphome/components/sgp40/* @SenexCrenshaw
|
||||
esphome/components/sgp4x/* @martgras @SenexCrenshaw
|
||||
esphome/components/sha256/* @esphome/core
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.12.4
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
+1
-14
@@ -857,20 +857,7 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
toolchain.create_factory_bin()
|
||||
toolchain.create_ota_bin()
|
||||
toolchain.create_elf_copy()
|
||||
from esphome.build_helpers.idedata import IDEDATA_BEST_EFFORT_ERRORS
|
||||
|
||||
try:
|
||||
if toolchain.get_idedata() is None:
|
||||
_LOGGER.warning("No idedata was generated for this build")
|
||||
except IDEDATA_BEST_EFFORT_ERRORS as err:
|
||||
# The firmware already built; an idedata failure must not fail
|
||||
# a successful build.
|
||||
_LOGGER.warning(
|
||||
"Could not generate idedata: %s (IDE, clang-tidy, and "
|
||||
"memory-analysis data will be unavailable for this build)",
|
||||
err,
|
||||
)
|
||||
_LOGGER.debug("Idedata failure detail", exc_info=True)
|
||||
toolchain.get_idedata()
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
|
||||
+33
-38
@@ -1,7 +1,6 @@
|
||||
"""ESP-IDF direct build generator for ESPHome."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.components.esp32 import (
|
||||
@@ -12,7 +11,6 @@ from esphome.components.esp32 import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
from esphome.espidf import variant_to_idf_target
|
||||
from esphome.framework_helpers import (
|
||||
get_project_compile_flags,
|
||||
get_project_cxx_compile_flags,
|
||||
@@ -20,8 +18,6 @@ from esphome.framework_helpers import (
|
||||
)
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
|
||||
# CXX_COMPILE_OPTIONS by project.cmake's __build_init) with the one set via
|
||||
# cg.set_cpp_standard(). Emitted between include(project.cmake) and project(),
|
||||
@@ -35,12 +31,11 @@ idf_build_set_property(CXX_COMPILE_OPTIONS "${{esphome_cxx_compile_options}}")""
|
||||
|
||||
|
||||
def get_available_components() -> list[str] | None:
|
||||
"""List the built-in ESP-IDF components from ``project_description.json``.
|
||||
"""Get list of built-in ESP-IDF components from project_description.json.
|
||||
|
||||
Only components below its ``idf_path/components`` count, which leaves out
|
||||
``src``, IDF-managed components, converted PIO libs and project local
|
||||
ones such as the Arduino ``component_stubs``. Returns ``None`` if the
|
||||
build dir or ``project_description.json`` isn't ready yet.
|
||||
Excludes ``src``, IDF-managed components (``managed_components/``), and
|
||||
converted PIO libs (``pio_components/``). Returns ``None`` if the build
|
||||
dir or ``project_description.json`` isn't ready yet.
|
||||
"""
|
||||
if CORE.build_path is None:
|
||||
return None
|
||||
@@ -51,24 +46,30 @@ def get_available_components() -> list[str] | None:
|
||||
try:
|
||||
with project_desc.open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
root = (Path(data["idf_path"]) / "components").resolve()
|
||||
result = [
|
||||
name
|
||||
for name, info in data.get("build_component_info", {}).items()
|
||||
if (comp_dir := info.get("dir"))
|
||||
and Path(comp_dir).resolve().is_relative_to(root)
|
||||
]
|
||||
except (json.JSONDecodeError, KeyError, OSError) as err:
|
||||
_LOGGER.debug("Could not read %s: %s", project_desc, err)
|
||||
|
||||
component_info = data.get("build_component_info", {})
|
||||
|
||||
result = []
|
||||
for name, info in component_info.items():
|
||||
# Exclude our own src component
|
||||
if name == "src":
|
||||
continue
|
||||
|
||||
# Exclude IDF-managed and converted-PIO components (external).
|
||||
comp_dir = info.get("dir", "")
|
||||
if "managed_components" in comp_dir or "pio_components" in comp_dir:
|
||||
continue
|
||||
|
||||
result.append(name)
|
||||
|
||||
return result
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return None
|
||||
if not result:
|
||||
_LOGGER.warning("No ESP-IDF components found under %s", root)
|
||||
return result
|
||||
|
||||
|
||||
def has_discovered_components() -> bool:
|
||||
"""Check if a previous configure discovered any built-in components."""
|
||||
return bool(get_available_components())
|
||||
"""Check if we have discovered components from a previous configure."""
|
||||
return get_available_components() is not None
|
||||
|
||||
|
||||
def _cmake_quote(value: str) -> str:
|
||||
@@ -78,17 +79,15 @@ def _cmake_quote(value: str) -> str:
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def get_project_cmakelists(
|
||||
minimal: bool = False, builtin_components: list[str] | None = None
|
||||
) -> str:
|
||||
def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
"""Generate the top-level CMakeLists.txt for ESP-IDF project.
|
||||
|
||||
When ``minimal`` is true, omit ``ESPHOME_PROJECT_BUILTIN_COMPONENTS``
|
||||
since ``project_description.json`` may be stale on the first write.
|
||||
``builtin_components`` supplies the discovered list (from the cache)
|
||||
instead of reading it from ``project_description.json``.
|
||||
"""
|
||||
idf_target = variant_to_idf_target(get_esp32_variant())
|
||||
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
|
||||
variant = get_esp32_variant()
|
||||
idf_target = variant.lower().replace("-", "")
|
||||
|
||||
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
|
||||
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
|
||||
@@ -163,11 +162,9 @@ def get_project_cmakelists(
|
||||
else "\n".join(
|
||||
f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)"
|
||||
for name in sorted(
|
||||
set(
|
||||
builtin_components
|
||||
if builtin_components is not None
|
||||
else get_available_components() or []
|
||||
).difference(CORE.cmake_args.get("EXCLUDE_COMPONENTS", "").split(";"))
|
||||
set(get_available_components() or []).difference(
|
||||
CORE.cmake_args.get("EXCLUDE_COMPONENTS", "").split(";")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -282,9 +279,7 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||
"""
|
||||
|
||||
|
||||
def write_project(
|
||||
minimal: bool = False, builtin_components: list[str] | None = None
|
||||
) -> None:
|
||||
def write_project(minimal: bool = False) -> None:
|
||||
"""Write ESP-IDF project files."""
|
||||
mkdir_p(CORE.build_path)
|
||||
mkdir_p(CORE.relative_src_path())
|
||||
@@ -292,7 +287,7 @@ def write_project(
|
||||
# Write top-level CMakeLists.txt
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("CMakeLists.txt"),
|
||||
get_project_cmakelists(minimal=minimal, builtin_components=builtin_components),
|
||||
get_project_cmakelists(minimal=minimal),
|
||||
)
|
||||
|
||||
# Write component CMakeLists.txt in src/
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Build helpers shared by the native (non-PlatformIO) toolchains."""
|
||||
@@ -1,24 +0,0 @@
|
||||
"""The PlatformIO-format size bar shared by the native toolchains."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def format_bar(used: int, total: int) -> str:
|
||||
"""Match PlatformIO's ``_format_availale_bytes`` (sic, pioupload.py) exactly."""
|
||||
pct_raw = used / total if total else 0
|
||||
blocks = 10
|
||||
filled = min(int(round(blocks * pct_raw)), blocks)
|
||||
progress = "=" * filled
|
||||
return (
|
||||
f"[{progress:<{blocks}}] {pct_raw: 6.1%} "
|
||||
f"(used {used:d} bytes from {total:d} bytes)"
|
||||
)
|
||||
|
||||
|
||||
def print_size_line(label: str, used: int, total: int) -> None:
|
||||
"""One PlatformIO-format summary line (``RAM``/``Flash``).
|
||||
|
||||
The label padding is part of the format: ``script/ci_memory_impact_extract.py``
|
||||
matches these lines verbatim.
|
||||
"""
|
||||
print(f"{label + ':':<7}{format_bar(used, total)}")
|
||||
@@ -232,7 +232,6 @@ enum SerialProxyPortType {
|
||||
message SerialProxyInfo {
|
||||
string name = 1; // Human-readable port name
|
||||
SerialProxyPortType port_type = 2; // Port type (RS232, RS485)
|
||||
uint32 configured_line_states = 3; // Bitmask of SerialProxyLineStateFlags this instance can drive
|
||||
}
|
||||
|
||||
// DeviceInfoResponse max_data_length values:
|
||||
@@ -2627,22 +2626,6 @@ message ZWaveProxyRequest {
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
enum ZWaveProxyStatus {
|
||||
ZWAVE_PROXY_STATUS_OK = 0; // Request completed successfully
|
||||
ZWAVE_PROXY_STATUS_IN_USE = 1; // Denied: another client is already subscribed
|
||||
ZWAVE_PROXY_STATUS_NOT_SUPPORTED = 2; // Request type not supported
|
||||
}
|
||||
|
||||
// Acknowledges a ZWaveProxyRequest (subscribe/unsubscribe). Sent since API 1.16.
|
||||
message ZWaveProxyRequestResponse {
|
||||
option (id) = 151;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_ZWAVE_PROXY";
|
||||
|
||||
ZWaveProxyRequestType type = 1; // Which request type this responds to
|
||||
ZWaveProxyStatus status = 2; // Result status
|
||||
}
|
||||
|
||||
// ==================== INFRARED ====================
|
||||
// Note: Feature and capability flag enums are defined in
|
||||
// esphome/components/infrared/infrared.h
|
||||
@@ -2786,18 +2769,12 @@ message SerialProxyGetModemPinsResponse {
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
|
||||
SerialProxyStatus status = 3; // INVALID_ARGUMENT if the instance index is out of range (since API 1.16)
|
||||
}
|
||||
|
||||
enum SerialProxyRequestType {
|
||||
SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance
|
||||
SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance
|
||||
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent)
|
||||
// Values below are only valid in SerialProxyRequestResponse.type, identifying which
|
||||
// operation is being acknowledged. Sending them in SerialProxyRequest.type is an
|
||||
// error the device answers with INVALID_ARGUMENT.
|
||||
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
|
||||
}
|
||||
|
||||
enum SerialProxyStatus {
|
||||
@@ -2806,8 +2783,6 @@ enum SerialProxyStatus {
|
||||
SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error
|
||||
SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed
|
||||
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance
|
||||
SERIAL_PROXY_STATUS_PORT_IN_USE = 5; // Denied: another client holds the port
|
||||
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
|
||||
}
|
||||
|
||||
// Generic request message for simple serial proxy operations
|
||||
@@ -2820,9 +2795,7 @@ message SerialProxyRequest {
|
||||
SerialProxyRequestType type = 2; // Request type
|
||||
}
|
||||
|
||||
// Acknowledges a serial proxy operation; the type field identifies which
|
||||
// operation is being acknowledged. Flush has been acknowledged since the
|
||||
// message was introduced; all other acknowledgements are sent since API 1.16.
|
||||
// Response to a SerialProxyRequest (e.g. flush completion or failure)
|
||||
message SerialProxyRequestResponse {
|
||||
option (id) = 147;
|
||||
option (source) = SOURCE_SERVER;
|
||||
|
||||
@@ -1381,12 +1381,7 @@ void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
|
||||
}
|
||||
|
||||
void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
ZWaveProxyRequestResponse resp{};
|
||||
resp.type = msg.type;
|
||||
resp.status = zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
|
||||
if (!this->send_message(resp)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "Z-Wave proxy response");
|
||||
}
|
||||
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1555,50 +1550,15 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
static enums::SerialProxyStatus serial_proxy_result_to_status(serial_proxy::SerialProxyResult result) {
|
||||
switch (result) {
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_OK:
|
||||
return enums::SERIAL_PROXY_STATUS_OK;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_ASSUMED_SUCCESS:
|
||||
return enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE:
|
||||
return enums::SERIAL_PROXY_STATUS_PORT_IN_USE;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT:
|
||||
return enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_TIMEOUT:
|
||||
return enums::SERIAL_PROXY_STATUS_TIMEOUT;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED:
|
||||
return enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_ERROR:
|
||||
return enums::SERIAL_PROXY_STATUS_ERROR;
|
||||
}
|
||||
return enums::SERIAL_PROXY_STATUS_ERROR; // Unreachable; all enum values handled above
|
||||
}
|
||||
|
||||
static void send_serial_proxy_ack(APIConnection *conn, uint32_t instance, enums::SerialProxyRequestType type,
|
||||
enums::SerialProxyStatus status) {
|
||||
SerialProxyRequestResponse resp{};
|
||||
resp.instance = instance;
|
||||
resp.type = type;
|
||||
resp.status = status;
|
||||
if (!conn->send_message(resp)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range (max %" PRIu32 ")", msg.instance,
|
||||
static_cast<uint32_t>(proxies.size()));
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE,
|
||||
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
|
||||
return;
|
||||
}
|
||||
serial_proxy::SerialProxyResult result = proxies[msg.instance]->configure(
|
||||
this, msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity), msg.stop_bits, msg.data_size);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE,
|
||||
serial_proxy_result_to_status(result));
|
||||
proxies[msg.instance]->configure(this, msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity),
|
||||
msg.stop_bits, msg.data_size);
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) {
|
||||
@@ -1614,30 +1574,20 @@ void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetM
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS,
|
||||
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
|
||||
return;
|
||||
}
|
||||
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_modem_pins(this, msg.line_states);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS,
|
||||
serial_proxy_result_to_status(result));
|
||||
proxies[msg.instance]->set_modem_pins(this, msg.line_states);
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
SerialProxyGetModemPinsResponse resp{};
|
||||
resp.instance = msg.instance;
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
// Pre-1.16 clients do not read the status field and would take this error
|
||||
// for a successful "both pins deasserted" answer; let them time out as before
|
||||
if (!this->client_supports_api_version(1, 16)) {
|
||||
return;
|
||||
}
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||
} else {
|
||||
resp.line_states = proxies[msg.instance]->get_modem_pins();
|
||||
return;
|
||||
}
|
||||
SerialProxyGetModemPinsResponse resp{};
|
||||
resp.instance = msg.instance;
|
||||
resp.line_states = proxies[msg.instance]->get_modem_pins();
|
||||
if (!this->send_message(resp)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
|
||||
}
|
||||
@@ -1647,31 +1597,40 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
send_serial_proxy_ack(this, msg.instance, msg.type, enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
|
||||
return;
|
||||
}
|
||||
auto *proxy = proxies[msg.instance];
|
||||
enums::SerialProxyStatus status;
|
||||
switch (msg.type) {
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
status = serial_proxy_result_to_status(proxy->serial_proxy_request(this, msg.type));
|
||||
proxies[msg.instance]->serial_proxy_request(this, msg.type);
|
||||
break;
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH:
|
||||
status = serial_proxy_result_to_status(proxy->flush_port(this));
|
||||
break;
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||
// Response-only discriminators; never valid in a request
|
||||
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
|
||||
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: {
|
||||
SerialProxyRequestResponse resp{};
|
||||
resp.instance = msg.instance;
|
||||
resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH;
|
||||
switch (proxies[msg.instance]->flush_port()) {
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_OK;
|
||||
break;
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
|
||||
break;
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT;
|
||||
break;
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
if (!this->send_message(resp)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
|
||||
status = enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
send_serial_proxy_ack(this, msg.instance, msg.type, status);
|
||||
}
|
||||
|
||||
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
|
||||
@@ -1790,17 +1749,15 @@ void APIConnection::complete_authentication_() {
|
||||
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
// Copy client name with truncation if needed (set_client_name handles truncation)
|
||||
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
||||
this->client_api_version_major_ =
|
||||
static_cast<uint8_t>(std::min<uint32_t>(msg.api_version_major, std::numeric_limits<uint8_t>::max()));
|
||||
this->client_api_version_minor_ =
|
||||
static_cast<uint8_t>(std::min<uint32_t>(msg.api_version_minor, std::numeric_limits<uint8_t>::max()));
|
||||
this->client_api_version_major_ = msg.api_version_major;
|
||||
this->client_api_version_minor_ = msg.api_version_minor;
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %u.%u", this->helper_->get_client_name(),
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
|
||||
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 16;
|
||||
resp.api_version_minor = 15;
|
||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||
resp.server_info = ESPHOME_VERSION_REF;
|
||||
resp.name = StringRef(App.get_name());
|
||||
@@ -1934,7 +1891,6 @@ bool APIConnection::send_device_info_response_() {
|
||||
auto &info = resp.serial_proxies[serial_proxy_index++];
|
||||
info.name = StringRef(proxy->get_name());
|
||||
info.port_type = proxy->get_port_type();
|
||||
info.configured_line_states = proxy->get_configured_modem_pins();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -1995,7 +1951,6 @@ bool APIConnection::send_device_capabilities_response_() {
|
||||
auto &info = resp.serial_proxies[serial_proxy_index++];
|
||||
info.name = StringRef(proxy->get_name());
|
||||
info.port_type = proxy->get_port_type();
|
||||
info.configured_line_states = proxy->get_configured_modem_pins();
|
||||
}
|
||||
#endif
|
||||
return this->send_message(resp);
|
||||
@@ -2226,7 +2181,7 @@ bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn,
|
||||
bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn,
|
||||
const void *msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise)
|
||||
@@ -2255,7 +2210,7 @@ uint16_t APIConnection::encode_to_buffer_slow(uint32_t calculated_size, MessageE
|
||||
APIConnection *conn, uint32_t remaining_size) {
|
||||
return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size);
|
||||
}
|
||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) {
|
||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
|
||||
|
||||
if (!this->try_to_clear_buffer(!is_log_message)) {
|
||||
@@ -2285,12 +2240,12 @@ void APIConnection::on_fatal_error() {
|
||||
this->flags_.remove = true;
|
||||
}
|
||||
|
||||
bool APIConnection::schedule_message_front_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size) {
|
||||
bool APIConnection::schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
||||
this->deferred_batch_.add_item_front(entity, message_type, estimated_size);
|
||||
return this->schedule_batch_();
|
||||
}
|
||||
|
||||
bool APIConnection::send_message_smart_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||
bool APIConnection::send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index) {
|
||||
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
||||
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
|
||||
@@ -326,10 +326,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
bool is_marked_for_removal() const { return this->flags_.remove; }
|
||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||
|
||||
// Get client API version for feature detection.
|
||||
// Stored versions saturate at 255 (see send_hello_response_), so requesting
|
||||
// a minimum above that can never match.
|
||||
bool client_supports_api_version(uint8_t major, uint8_t minor) const {
|
||||
// Get client API version for feature detection
|
||||
bool client_supports_api_version(uint16_t major, uint16_t minor) const {
|
||||
return this->client_api_version_major_ > major ||
|
||||
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
|
||||
}
|
||||
@@ -376,7 +374,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
return true;
|
||||
return this->try_to_clear_buffer_slow_(log_out_of_space);
|
||||
}
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type);
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type);
|
||||
|
||||
const char *get_name() const { return this->helper_->get_client_name(); }
|
||||
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
|
||||
@@ -425,7 +423,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
}
|
||||
|
||||
// Non-template buffer management for send_message
|
||||
bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||
|
||||
// Core batch encoding logic. ALWAYS_INLINE so encode_fn devirtualizes at hot call sites.
|
||||
// Defined in api_connection_buffer.h (needs APIServer complete).
|
||||
@@ -666,9 +664,10 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
struct BatchItem {
|
||||
EntityBase *entity; // 4 bytes - Entity pointer
|
||||
uint16_t message_type; // 2 bytes - Message type for protocol and dispatch
|
||||
uint8_t message_type; // 1 byte - Message type for protocol and dispatch
|
||||
uint8_t estimated_size; // 1 byte - Estimated message size (max 255 bytes)
|
||||
uint8_t aux_data_index{AUX_DATA_UNUSED}; // 1 byte - For events: index into entity's event_types
|
||||
// 1 byte padding
|
||||
};
|
||||
|
||||
std::vector<BatchItem> items;
|
||||
@@ -678,7 +677,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// connections that do, buffers are released after initial sync anyway
|
||||
|
||||
// Add item to the batch (with deduplication)
|
||||
void add_item(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||
void add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index = AUX_DATA_UNUSED) {
|
||||
// Dedup: O(n) scan but optimized for RAM over performance
|
||||
// Skip deduplication for events - they are edge-triggered, every occurrence matters
|
||||
@@ -694,7 +693,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->items.push_back({entity, message_type, estimated_size, aux_data_index});
|
||||
}
|
||||
// Add item to the front of the batch (for high priority messages like ping)
|
||||
void add_item_front(EntityBase *entity, uint16_t message_type, uint8_t estimated_size) {
|
||||
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
||||
// Swap to front avoids expensive vector::insert which shifts all elements
|
||||
this->items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
||||
if (this->items.size() > 1) {
|
||||
@@ -759,15 +758,13 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif
|
||||
} flags_{}; // 2 bytes total
|
||||
|
||||
// 2-byte type immediately after flags_ (no padding between them)
|
||||
uint16_t batch_message_type_{0}; // Current message type during batch encoding
|
||||
// 2-byte types immediately after flags_ (no padding between them)
|
||||
uint16_t client_api_version_major_{0};
|
||||
uint16_t client_api_version_minor_{0};
|
||||
// 1-byte types to fill remaining space before next 4-byte boundary
|
||||
// Client API versions are clamped to 255 on receive (see send_hello_response_)
|
||||
uint8_t client_api_version_major_{0};
|
||||
uint8_t client_api_version_minor_{0};
|
||||
ActiveIterator active_iterator_{ActiveIterator::NONE};
|
||||
// Total: 2 (flags) + 2 + 1 + 1 + 1 + 1 (batch_header_size_ below) = 8 bytes,
|
||||
// aligned to 4-byte boundary
|
||||
uint8_t batch_message_type_{0}; // Current message type during batch encoding
|
||||
// Total: 2 (flags) + 2 + 2 + 1 + 1 = 8 bytes, aligned to 4-byte boundary
|
||||
|
||||
// Actual header size used by encode_to_buffer for the current message.
|
||||
// Read by process_batch_multi_ to pass into MessageInfo.
|
||||
@@ -816,7 +813,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// 2. It's an EventResponse (events are edge-triggered - every occurrence matters)
|
||||
// 3. OR: User has opted into immediate sending (should_try_send_immediately = true
|
||||
// AND batch_delay = 0)
|
||||
inline bool should_send_immediately_(uint16_t message_type) const {
|
||||
inline bool should_send_immediately_(uint8_t message_type) const {
|
||||
return (
|
||||
#ifdef USE_UPDATE
|
||||
message_type == UpdateStateResponse::MESSAGE_TYPE ||
|
||||
@@ -830,11 +827,11 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Helper method to send a message either immediately or via batching
|
||||
// Tries immediate send if should_send_immediately_() returns true and buffer has space
|
||||
// Falls back to batching if immediate send fails or isn't applicable
|
||||
bool send_message_smart_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||
bool send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED);
|
||||
|
||||
// Helper function to schedule a deferred message with known message type
|
||||
bool schedule_message_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||
bool schedule_message_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) {
|
||||
this->deferred_batch_.add_item(entity, message_type, estimated_size, aux_data_index);
|
||||
return this->schedule_batch_();
|
||||
@@ -842,7 +839,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
// Helper function to schedule a high priority message at the front of the batch
|
||||
// Out-of-line: callers (on_shutdown, check_keepalive_) are cold paths
|
||||
bool schedule_message_front_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size);
|
||||
bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
|
||||
|
||||
// Helper function to log client messages with name and peername
|
||||
void log_client_(int level, const LogString *message);
|
||||
|
||||
@@ -49,16 +49,16 @@ struct ReadPacketBuffer {
|
||||
};
|
||||
|
||||
// Packed message info structure to minimize memory usage
|
||||
// message_type matches the wire formats: noise carries a fixed 16-bit type
|
||||
// field, plaintext a type varint. The proto codegen caps message IDs at 16383
|
||||
// so the plaintext type varint fits the 2 bytes budgeted in HEADER_PADDING.
|
||||
// Note: message_type is uint8_t — all current protobuf message types fit in 8 bits.
|
||||
// The noise wire format encodes types as 16-bit, but the high byte is always 0.
|
||||
// If message types ever exceed 255, this and encrypt_noise_message_ must be updated.
|
||||
struct MessageInfo {
|
||||
uint16_t offset; // Offset in buffer where message starts
|
||||
uint16_t payload_size; // Size of the message payload
|
||||
uint16_t message_type; // Message type (0-16383)
|
||||
uint8_t message_type; // Message type (0-255)
|
||||
uint8_t header_size; // Actual header size used (avoids recomputation in write path)
|
||||
|
||||
MessageInfo(uint16_t type, uint16_t off, uint16_t size, uint8_t hdr)
|
||||
MessageInfo(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr)
|
||||
: offset(off), payload_size(size), message_type(type), header_size(hdr) {}
|
||||
};
|
||||
|
||||
@@ -173,7 +173,7 @@ class APIFrameHelper {
|
||||
}
|
||||
// Write a single protobuf message - the hot path (87-100% of all writes).
|
||||
// Caller must ensure state is DATA before calling.
|
||||
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf messages in a single batched operation.
|
||||
// Caller must ensure state is DATA and messages is not empty.
|
||||
// messages contains (message_type, offset, length) for each message in the buffer.
|
||||
@@ -187,15 +187,15 @@ class APIFrameHelper {
|
||||
// Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC
|
||||
// footer, plaintext has footer=0). If a protocol with a plaintext footer is ever
|
||||
// added, this should become a virtual method.
|
||||
uint8_t frame_header_size(uint16_t payload_size, uint16_t message_type) const {
|
||||
uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const {
|
||||
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
||||
return this->frame_footer_size_
|
||||
? this->frame_header_padding_
|
||||
: static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint16(message_type));
|
||||
: static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
|
||||
#elif defined(USE_API_NOISE)
|
||||
return this->frame_header_padding_;
|
||||
#else // USE_API_PLAINTEXT only
|
||||
return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint16(message_type));
|
||||
return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
|
||||
#endif
|
||||
}
|
||||
// Get the frame footer size required by this protocol
|
||||
|
||||
@@ -442,7 +442,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
}
|
||||
// Encrypt a single noise message in place and return the encrypted frame length.
|
||||
// Returns APIError::OK on success.
|
||||
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint16_t message_type,
|
||||
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
|
||||
uint16_t &encrypted_len_out) {
|
||||
// The noise frame header is written after encryption, when the size is known
|
||||
|
||||
@@ -472,7 +472,7 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
|
||||
@@ -31,7 +31,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
#endif
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
@@ -44,7 +44,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError state_action_handshake_write_();
|
||||
APIError try_read_frame_();
|
||||
APIError write_frame_(const uint8_t *data, uint16_t len);
|
||||
APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint16_t message_type,
|
||||
APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
|
||||
uint16_t &encrypted_len_out);
|
||||
APIError init_handshake_();
|
||||
APIError check_handshake_finished_();
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "api_pb2.h"
|
||||
#include "proto.h"
|
||||
#include <cstring>
|
||||
#include <cinttypes>
|
||||
@@ -253,21 +252,24 @@ ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_
|
||||
*p = static_cast<uint8_t>(value);
|
||||
}
|
||||
|
||||
// The generator rejects message IDs above MAX_MESSAGE_TYPE, so the type varint
|
||||
// can never outgrow the 2 bytes HEADER_PADDING budgets for it. Without this
|
||||
// bound, write_plaintext_header's header_offset would underflow for the first
|
||||
// message in a batch and the header write would land outside the buffer.
|
||||
static_assert(1 + 3 + ProtoSize::varint16(MAX_MESSAGE_TYPE) <= APIPlaintextFrameHelper::HEADER_PADDING,
|
||||
"HEADER_PADDING cannot fit the type varint of the largest message ID");
|
||||
// Encode an 8-bit varint (1-2 bytes) using pre-computed length.
|
||||
ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t varint_len, uint8_t *p) {
|
||||
if (varint_len == 2) {
|
||||
*p++ = static_cast<uint8_t>(value | 0x80);
|
||||
*p = static_cast<uint8_t>(value >> 7);
|
||||
} else {
|
||||
*p = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Write plaintext header into pre-allocated padding before payload.
|
||||
// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg,
|
||||
// actual header size for contiguous batch messages).
|
||||
// Returns the total header length (indicator + varints).
|
||||
ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, uint16_t payload_size,
|
||||
uint16_t message_type, uint8_t padding_size) {
|
||||
uint8_t message_type, uint8_t padding_size) {
|
||||
uint8_t size_varint_len = ProtoSize::varint16(payload_size);
|
||||
uint8_t type_varint_len = ProtoSize::varint16(message_type);
|
||||
uint8_t type_varint_len = ProtoSize::varint8(message_type);
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// The header is right-justified within the padding so it sits immediately before payload.
|
||||
@@ -290,12 +292,12 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_
|
||||
|
||||
// Encode varints directly into buffer using pre-computed lengths
|
||||
encode_varint_16(payload_size, size_varint_len, buf_start + header_offset + 1);
|
||||
encode_varint_16(message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len);
|
||||
encode_varint_8(message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len);
|
||||
|
||||
return total_header_len;
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
|
||||
@@ -10,8 +10,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
// Plaintext header structure (worst case):
|
||||
// Pos 0: indicator (0x00)
|
||||
// Pos 1-3: payload size varint (up to 3 bytes)
|
||||
// Pos 4-5: message type varint (up to 2 bytes; covers message IDs up to
|
||||
// 16383, enforced by the proto codegen)
|
||||
// Pos 4-5: message type varint (up to 2 bytes)
|
||||
// Pos 6+: actual payload data
|
||||
static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint
|
||||
|
||||
@@ -22,7 +21,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
#ifdef USE_API_NOISE
|
||||
// After try_read_frame_ returned PROTOCOL_SWITCH_TO_NOISE: copy out the
|
||||
|
||||
@@ -102,14 +102,12 @@ uint8_t *SerialProxyInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PAR
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->port_type));
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->configured_line_states);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SerialProxyInfo::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_length(1, this->name.size());
|
||||
size += this->port_type ? 2 : 0;
|
||||
size += ProtoSize::calc_uint32(1, this->configured_line_states);
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
@@ -3944,18 +3942,6 @@ uint32_t ZWaveProxyRequest::calculate_size() const {
|
||||
size += ProtoSize::calc_length(1, this->data_len);
|
||||
return size;
|
||||
}
|
||||
uint8_t *ZWaveProxyRequestResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->type));
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->status));
|
||||
return pos;
|
||||
}
|
||||
uint32_t ZWaveProxyRequestResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += this->type ? 2 : 0;
|
||||
size += this->status ? 2 : 0;
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
uint8_t *ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -4198,14 +4184,12 @@ uint8_t *SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer PROTO_
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->line_states);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, static_cast<uint32_t>(this->status));
|
||||
return pos;
|
||||
}
|
||||
uint32_t SerialProxyGetModemPinsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, this->instance);
|
||||
size += ProtoSize::calc_uint32(1, this->line_states);
|
||||
size += this->status ? 2 : 0;
|
||||
return size;
|
||||
}
|
||||
bool SerialProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
|
||||
+140
-172
File diff suppressed because it is too large
Load Diff
@@ -816,18 +816,6 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::ZWaveProxyStatus>(enums::ZWaveProxyStatus value) {
|
||||
switch (value) {
|
||||
case enums::ZWAVE_PROXY_STATUS_OK:
|
||||
return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_OK");
|
||||
case enums::ZWAVE_PROXY_STATUS_IN_USE:
|
||||
return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_IN_USE");
|
||||
case enums::ZWAVE_PROXY_STATUS_NOT_SUPPORTED:
|
||||
return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_NOT_SUPPORTED");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) {
|
||||
@@ -850,10 +838,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_FLUSH");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
@@ -870,10 +854,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_TIMEOUT");
|
||||
case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_NOT_SUPPORTED");
|
||||
case enums::SERIAL_PROXY_STATUS_PORT_IN_USE:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_PORT_IN_USE");
|
||||
case enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_INVALID_ARGUMENT");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
@@ -934,7 +914,6 @@ const char *SerialProxyInfo::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyInfo"));
|
||||
dump_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("port_type"), static_cast<enums::SerialProxyPortType>(this->port_type));
|
||||
dump_field(out, ESPHOME_PSTR("configured_line_states"), this->configured_line_states);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
@@ -2665,12 +2644,6 @@ const char *ZWaveProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ZWaveProxyRequestResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyRequestResponse"));
|
||||
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ZWaveProxyRequestType>(this->type));
|
||||
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::ZWaveProxyStatus>(this->status));
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const {
|
||||
@@ -2780,7 +2753,6 @@ const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsResponse"));
|
||||
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
|
||||
dump_field(out, ESPHOME_PSTR("line_states"), this->line_states);
|
||||
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::SerialProxyStatus>(this->status));
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -684,6 +684,11 @@ class ProtoSize {
|
||||
return value < VARINT_THRESHOLD_1_BYTE ? 1 : (value < VARINT_THRESHOLD_2_BYTE ? 2 : 3);
|
||||
}
|
||||
|
||||
// Varint encoded length for an 8-bit value (1 or 2 bytes).
|
||||
static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint8(uint8_t value) {
|
||||
return value < VARINT_THRESHOLD_1_BYTE ? 1 : 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
*
|
||||
|
||||
@@ -7,7 +7,6 @@ from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
include_builtin_idf_component,
|
||||
require_certificate_bundle,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -336,8 +335,6 @@ def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> N
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_http_client")
|
||||
# HTTPS streams verify the server against the root certificate bundle
|
||||
require_certificate_bundle()
|
||||
|
||||
add_idf_component(
|
||||
name="esphome/esp-audio-libs",
|
||||
|
||||
@@ -30,9 +30,8 @@ void AudioHTTPMediaSource::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Audio HTTP Media Source:\n"
|
||||
" Buffer Size: %zu bytes\n"
|
||||
" Persistent Ring Buffer: %s\n"
|
||||
" Decoder Task Stack in PSRAM: %s",
|
||||
this->buffer_size_, YESNO(this->persistent_ring_buffer_), YESNO(this->decoder_task_stack_in_psram_));
|
||||
this->buffer_size_, YESNO(this->decoder_task_stack_in_psram_));
|
||||
}
|
||||
|
||||
void AudioHTTPMediaSource::setup() {
|
||||
@@ -40,7 +39,6 @@ void AudioHTTPMediaSource::setup() {
|
||||
|
||||
micro_decoder::DecoderConfig config;
|
||||
config.ring_buffer_size = this->buffer_size_;
|
||||
config.persistent_ring_buffer = this->persistent_ring_buffer_;
|
||||
// Keep the transfer buffer smaller than the ring buffer so the reader can top up the ring
|
||||
// while the decoder is still draining it, instead of oscillating between empty and full.
|
||||
config.transfer_buffer_size = std::min(DEFAULT_TRANSFER_BUFFER_SIZE, this->buffer_size_ / 2);
|
||||
|
||||
@@ -33,7 +33,6 @@ class AudioHTTPMediaSource final : public Component,
|
||||
|
||||
void set_buffer_size(size_t buffer_size) { this->buffer_size_ = buffer_size; }
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->decoder_task_stack_in_psram_ = task_stack_in_psram; }
|
||||
void set_persistent_ring_buffer(bool persistent) { this->persistent_ring_buffer_ = persistent; }
|
||||
|
||||
// MediaSource interface implementation
|
||||
bool play_uri(const std::string &uri) override;
|
||||
@@ -55,7 +54,6 @@ class AudioHTTPMediaSource final : public Component,
|
||||
// on_audio_write(). Must be atomic to avoid a data race.
|
||||
std::atomic<bool> pause_{false};
|
||||
bool decoder_task_stack_in_psram_{false};
|
||||
bool persistent_ring_buffer_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::audio_http
|
||||
|
||||
@@ -7,8 +7,6 @@ from esphome.types import ConfigType
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
AUTO_LOAD = ["audio"]
|
||||
|
||||
CONF_PERSISTENT_RING_BUFFER = "persistent_ring_buffer"
|
||||
|
||||
audio_http_ns = cg.esphome_ns.namespace("audio_http")
|
||||
AudioHTTPMediaSource = audio_http_ns.class_(
|
||||
"AudioHTTPMediaSource", cg.Component, media_source.MediaSource
|
||||
@@ -30,7 +28,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
min=5000, max=1000000
|
||||
),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
|
||||
cv.Optional(CONF_PERSISTENT_RING_BUFFER, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
@@ -48,4 +45,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_task_stack_in_psram(True))
|
||||
psram.request_external_task_stack()
|
||||
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||
cg.add(var.set_persistent_ring_buffer(config[CONF_PERSISTENT_RING_BUFFER]))
|
||||
|
||||
@@ -4,6 +4,7 @@ import re
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from esphome import git
|
||||
@@ -12,7 +13,7 @@ from esphome.components.packages import validate_source_shorthand
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ESPHOME, CONF_PROJECT, CONF_REF, CONF_WIFI
|
||||
import esphome.final_validate as fv
|
||||
from esphome.net_retry import fetch_with_retry, http_request
|
||||
from esphome.happy_eyeballs import ensure_happy_eyeballs
|
||||
from esphome.types import ConfigType
|
||||
from esphome.yaml_util import dump
|
||||
|
||||
@@ -110,20 +111,14 @@ def import_config(
|
||||
|
||||
if git_file.query and "full_config" in git_file.query:
|
||||
url = git_file.raw_url
|
||||
|
||||
# Deferred so config-time imports of this component stay light;
|
||||
# http_request does the lazy import for the request itself.
|
||||
import requests
|
||||
|
||||
def _fetch() -> str:
|
||||
req = http_request("GET", url, timeout=30)
|
||||
req.raise_for_status()
|
||||
return req.text
|
||||
|
||||
try:
|
||||
contents = fetch_with_retry(url, _fetch, what="Import")
|
||||
ensure_happy_eyeballs()
|
||||
req = requests.get(url, timeout=30)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise ValueError(f"Error while fetching {url}: {e}") from e
|
||||
|
||||
contents = req.text
|
||||
yaml = YAML()
|
||||
loaded_yaml = yaml.load(contents)
|
||||
if (
|
||||
|
||||
@@ -7,10 +7,8 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_FREQUENCY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -20,10 +18,8 @@ from esphome.const import (
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_EMPTY,
|
||||
UNIT_HERTZ,
|
||||
UNIT_PULSES,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
@@ -33,32 +29,6 @@ from .. import CONF_EMONTX_ID, CONF_TAG_NAME, EmonTx, emontx_ns
|
||||
|
||||
EmonTxSensor = emontx_ns.class_("EmonTxSensor", sensor.Sensor, cg.Component)
|
||||
|
||||
# Known emonTx/avrdb JSON tag conventions, gathered from real firmware
|
||||
# (see https://github.com/openenergymonitor/avrdb_firmware), used to decide
|
||||
# whether each tag below requires a numeric index or may also appear bare:
|
||||
#
|
||||
# Tag family Bare (no index) Numeric-indexed
|
||||
# ----------- ----------------------- ----------------------------------
|
||||
# P (power) no P1, P2, ... (multi-channel boards)
|
||||
# E (energy) no E1, E2, ...
|
||||
# V (voltage) Vrms (NOT matched here, V1, V2, V3 (per-phase boards)
|
||||
# doesn't fit "V"+digits)
|
||||
# I (current) no I1, I2, ...
|
||||
# T (temp.) no T1, T2, ...
|
||||
# F (frequency) F (single mains freq.) not seen indexed
|
||||
# PULSE pulse (single-CT boards) PULSE1, PULSE2, ... (other variants)
|
||||
# PF (power not seen bare PF1, PF2, ... (currently unused/
|
||||
# factor) commented out in avrdb firmware)
|
||||
# AP (apparent not seen bare AP1, AP2, ... (not an avrdb tag at
|
||||
# power) all; avrdb uses "VA"+index instead,
|
||||
# itself currently unused/commented
|
||||
# out; "AP" is kept here for other
|
||||
# firmware/integrations using it)
|
||||
#
|
||||
# This is why a bare "PULSE" resolves to proper defaults below, but bare
|
||||
# "PF"/"AP" fall back to generic defaults instead: only PULSE has a
|
||||
# confirmed bare-tag use in real, currently-shipping firmware.
|
||||
|
||||
# Define sensor type configurations by prefix
|
||||
SENSOR_CONFIGS = {
|
||||
"P": {
|
||||
@@ -93,25 +63,7 @@ SENSOR_CONFIGS = {
|
||||
},
|
||||
}
|
||||
|
||||
# Tags reported once, without a numeric index (e.g. "F"), matched exactly
|
||||
# rather than by prefix.
|
||||
EXACT_TAG_CONFIGS = {
|
||||
"F": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_HERTZ,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_FREQUENCY,
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 2,
|
||||
},
|
||||
}
|
||||
|
||||
# Pattern-based configurations. The remainder after the prefix must be a
|
||||
# non-empty numeric index (like V1/I1/E1), so e.g. "APPLE" doesn't collide
|
||||
# with the "AP" prefix and a bare "PF"/"AP" (no index) doesn't match.
|
||||
# "PULSE" is the exception: some emonTx firmware (e.g. avrdb-based single-CT
|
||||
# variants) reports a single pulse counter as a bare "pulse" tag with no
|
||||
# numeric index at all, so that pattern also accepts an empty suffix.
|
||||
PATTERNS_ALLOWING_BARE_TAG = {"PULSE"}
|
||||
|
||||
# Pattern-based configurations
|
||||
PATTERN_CONFIGS = {
|
||||
"PULSE": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_PULSES,
|
||||
@@ -125,21 +77,14 @@ PATTERN_CONFIGS = {
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 2,
|
||||
},
|
||||
"AP": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_VOLT_AMPS,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_APPARENT_POWER,
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 2,
|
||||
},
|
||||
}
|
||||
|
||||
# BASE_SCHEMA intentionally omits state_class and accuracy_decimals defaults.
|
||||
# Passing them to sensor_schema() would register them via cv.Optional(key, default=...),
|
||||
# making them always present in the validated config dict and preventing
|
||||
# apply_tag_defaults from overriding them with the correct per-prefix values.
|
||||
# They are injected by apply_tag_defaults below, after running through the
|
||||
# same validators sensor_schema() would use (see _DEFAULT_VALIDATORS) so the
|
||||
# values are code-generation-ready.
|
||||
# They are injected by apply_tag_defaults below, after running through
|
||||
# sensor.validate_state_class() so the value is code-generation-ready.
|
||||
BASE_SCHEMA = sensor.sensor_schema(EmonTxSensor).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_EMONTX_ID): cv.use_id(EmonTx),
|
||||
@@ -148,43 +93,30 @@ BASE_SCHEMA = sensor.sensor_schema(EmonTxSensor).extend(
|
||||
)
|
||||
|
||||
|
||||
_DEFAULT_VALIDATORS = {
|
||||
CONF_STATE_CLASS: sensor.validate_state_class,
|
||||
CONF_DEVICE_CLASS: sensor.validate_device_class,
|
||||
CONF_UNIT_OF_MEASUREMENT: sensor.validate_unit_of_measurement,
|
||||
}
|
||||
|
||||
|
||||
def _apply_defaults(config: ConfigType, defaults: dict) -> None:
|
||||
"""Inject defaults into config, skipping keys already set by the user.
|
||||
Values are run through the same validators sensor_schema() would use, so
|
||||
they are code-generation-ready and a typo'd constant fails validation
|
||||
instead of shipping silently."""
|
||||
state_class values are run through validate_state_class so they are
|
||||
code-generation-ready, matching what sensor_schema() would normally do."""
|
||||
for key, value in defaults.items():
|
||||
if key not in config:
|
||||
if key in _DEFAULT_VALIDATORS:
|
||||
value = _DEFAULT_VALIDATORS[key](value)
|
||||
if key == CONF_STATE_CLASS:
|
||||
value = sensor.validate_state_class(value)
|
||||
config[key] = value
|
||||
|
||||
|
||||
def apply_tag_defaults(config: ConfigType) -> ConfigType:
|
||||
"""Apply defaults based on tag prefix if applicable, but don't restrict any tags."""
|
||||
tag = config[CONF_TAG_NAME]
|
||||
tag_upper = tag.upper()
|
||||
|
||||
if (exact_config := EXACT_TAG_CONFIGS.get(tag_upper)) is not None:
|
||||
_apply_defaults(config, exact_config)
|
||||
return config
|
||||
|
||||
for pattern, pattern_config in PATTERN_CONFIGS.items():
|
||||
suffix = tag_upper[len(pattern) :]
|
||||
bare_ok = not suffix and pattern in PATTERNS_ALLOWING_BARE_TAG
|
||||
if tag_upper.startswith(pattern) and (suffix.isdigit() or bare_ok):
|
||||
_apply_defaults(config, pattern_config)
|
||||
return config
|
||||
|
||||
# Only apply defaults for known prefixes with numeric indices (e.g. E1, V2, T3)
|
||||
if len(tag) >= 2:
|
||||
tag_upper = tag.upper()
|
||||
|
||||
for pattern, pattern_config in PATTERN_CONFIGS.items():
|
||||
if tag_upper.startswith(pattern):
|
||||
_apply_defaults(config, pattern_config)
|
||||
return config
|
||||
|
||||
# Only apply defaults for known prefixes with numeric indices (e.g. E1, V2, T3)
|
||||
prefix = tag_upper[0]
|
||||
if prefix in SENSOR_CONFIGS and tag[1:].isdigit():
|
||||
_apply_defaults(config, SENSOR_CONFIGS[prefix])
|
||||
|
||||
@@ -65,7 +65,6 @@ from .boards import BOARDS, STANDARD_BOARDS
|
||||
from .const import (
|
||||
KEY_ARDUINO_LIBRARIES,
|
||||
KEY_BOARD,
|
||||
KEY_CERT_BUNDLE,
|
||||
KEY_COMPONENTS,
|
||||
KEY_ESP32,
|
||||
KEY_EXCLUDE_COMPONENTS,
|
||||
@@ -238,7 +237,6 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"esp_gdbstub", # GDB stub panic handler - unused by ESPHome; bt pulls it back
|
||||
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
|
||||
"esp_http_client", # HTTP client - only needed by http_request component
|
||||
"esp_http_server", # HTTP server - re-included by web_server_idf, esp32_camera_web_server
|
||||
"esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation
|
||||
"esp_https_server", # HTTPS server - ESPHome has its own web server
|
||||
"esp_lcd", # LCD controller drivers - only needed by display component
|
||||
@@ -247,7 +245,6 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
|
||||
"json", # cJSON library - ESPHome uses ArduinoJson instead
|
||||
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
|
||||
"nvs_sec_provider", # NVS encryption key provider - re-included when CONFIG_NVS_ENCRYPTION is set
|
||||
"openthread", # Thread protocol - only needed by openthread component
|
||||
"perfmon", # Xtensa performance monitor - ESPHome has its own debug component
|
||||
"protobuf-c", # Protobuf runtime - only used by provisioning components (also excluded)
|
||||
@@ -346,10 +343,6 @@ ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = {
|
||||
"Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"),
|
||||
}
|
||||
|
||||
# Arduino libraries whose sources reference esp_crt_bundle_attach without a
|
||||
# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE guard, so enabling them needs the bundle.
|
||||
ARDUINO_LIBRARIES_NEEDING_CERT_BUNDLE = frozenset({"NetworkClientSecure"})
|
||||
|
||||
# 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
|
||||
@@ -651,27 +644,6 @@ class RawSdkconfigValue:
|
||||
SdkconfigValueType = bool | int | HexInt | str | RawSdkconfigValue
|
||||
|
||||
|
||||
def is_idf_sdkconfig_option_enabled(name: str) -> bool:
|
||||
"""Return True when a bool sdkconfig option resolves to ``y``.
|
||||
|
||||
Handles both the ``True`` a component sets and the raw ``y`` a user sets
|
||||
in ``sdkconfig_options``.
|
||||
"""
|
||||
value = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(name)
|
||||
return value is not None and _format_sdkconfig_val(value) == "y"
|
||||
|
||||
|
||||
def set_idf_sdkconfig_default(name: str, value: SdkconfigValueType) -> None:
|
||||
"""Set an sdkconfig option unless it is already set.
|
||||
|
||||
For the FINAL priority reconcile jobs: they run after every to_code,
|
||||
including the user's sdkconfig_options, and must not override an
|
||||
existing value.
|
||||
"""
|
||||
if name not in CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]:
|
||||
add_idf_sdkconfig_option(name, value)
|
||||
|
||||
|
||||
def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
|
||||
"""Set an esp-idf sdkconfig value."""
|
||||
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS][name] = value
|
||||
@@ -816,10 +788,6 @@ def _enable_arduino_library(name: str) -> None:
|
||||
# Also enable any required IDF components
|
||||
for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()):
|
||||
include_builtin_idf_component(idf_component)
|
||||
if not ARDUINO_LIBRARIES_NEEDING_CERT_BUNDLE.isdisjoint(
|
||||
{name, *ARDUINO_LIBRARY_DEPENDENCIES.get(name, ())}
|
||||
):
|
||||
require_certificate_bundle()
|
||||
|
||||
|
||||
def add_extra_script(stage: str, filename: str, path: Path):
|
||||
@@ -1767,16 +1735,6 @@ def require_vfs_termios() -> None:
|
||||
CORE.data[KEY_VFS_TERMIOS_REQUIRED] = True
|
||||
|
||||
|
||||
def require_certificate_bundle() -> None:
|
||||
"""Enable the mbedTLS root certificate bundle for this build.
|
||||
|
||||
The bundle is off by default; components that verify TLS server
|
||||
certificates (http_request, audio streaming) call this so the bundle is
|
||||
compiled and gen_crt_bundle runs only when something uses it.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_CERT_BUNDLE] = True
|
||||
|
||||
|
||||
def require_full_certificate_bundle() -> None:
|
||||
"""Request the full certificate bundle instead of the common-CAs-only bundle.
|
||||
|
||||
@@ -1786,7 +1744,6 @@ def require_full_certificate_bundle() -> None:
|
||||
|
||||
Call this from components that need to connect to services using uncommon CAs.
|
||||
"""
|
||||
require_certificate_bundle()
|
||||
CORE.data[KEY_ESP32][KEY_FULL_CERT_BUNDLE] = True
|
||||
|
||||
|
||||
@@ -2203,10 +2160,6 @@ def register_exclude_components_cmake_arg() -> None:
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _write_exclude_components() -> None:
|
||||
"""Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions."""
|
||||
# NVS encryption needs nvs_sec_provider however it was enabled: the
|
||||
# nvs_encryption option, raw sdkconfig_options or another component.
|
||||
if is_idf_sdkconfig_option_enabled("CONFIG_NVS_ENCRYPTION"):
|
||||
include_builtin_idf_component("nvs_sec_provider")
|
||||
register_exclude_components_cmake_arg()
|
||||
|
||||
|
||||
@@ -2265,31 +2218,6 @@ async def _set_libc_picolibc_newlib_compat() -> None:
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _reconcile_certificate_bundle_sdkconfig() -> None:
|
||||
"""Enable the mbedTLS certificate bundle only when something asked for it.
|
||||
|
||||
Runs at FINAL priority so every require_certificate_bundle() call has
|
||||
happened. Without a request the bundle is disabled, which skips
|
||||
esp_crt_bundle.c, the gen_crt_bundle step and the x509_crt_bundle.S embed.
|
||||
A user-supplied sdkconfig_options value takes precedence.
|
||||
"""
|
||||
data = CORE.data[KEY_ESP32]
|
||||
enabled = data.get(KEY_CERT_BUNDLE, False)
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", enabled)
|
||||
if not enabled:
|
||||
return
|
||||
# Use CMN (common CAs) bundle by default to save ~51KB flash
|
||||
# CMN covers CAs with >1% market share (~99% of websites)
|
||||
# Components needing uncommon CAs can call require_full_certificate_bundle()
|
||||
use_full_bundle = data.get(KEY_FULL_CERT_BUNDLE, False)
|
||||
set_idf_sdkconfig_default(
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL", use_full_bundle
|
||||
)
|
||||
if not use_full_bundle:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _reconcile_network_sdkconfig() -> None:
|
||||
"""Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags.
|
||||
@@ -2301,31 +2229,37 @@ async def _reconcile_network_sdkconfig() -> None:
|
||||
always takes precedence.
|
||||
"""
|
||||
net = CORE.data[KEY_ESP32].get(KEY_NETWORK_SDKCONFIG, NetworkSdkconfigData())
|
||||
opts = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
is_arduino = CORE.using_arduino
|
||||
|
||||
def set_opt(name: str, value: SdkconfigValueType) -> None:
|
||||
# User sdkconfig_options (applied during to_code) win.
|
||||
if name not in opts:
|
||||
add_idf_sdkconfig_option(name, value)
|
||||
|
||||
# Bluetooth: only ever enable when requested. The IDF default is off.
|
||||
# According to the IDF docs, only one of 4.2 or 5.0 should be enabled.
|
||||
if net.bluetooth:
|
||||
set_idf_sdkconfig_default("CONFIG_BT_ENABLED", True)
|
||||
set_idf_sdkconfig_default("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
|
||||
set_idf_sdkconfig_default("CONFIG_BT_BLE_50_FEATURES_SUPPORTED", False)
|
||||
set_opt("CONFIG_BT_ENABLED", True)
|
||||
set_opt("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
|
||||
set_opt("CONFIG_BT_BLE_50_FEATURES_SUPPORTED", False)
|
||||
|
||||
# WiFi stack: disable only when Ethernet is present and WiFi is not. WiFi
|
||||
# relies on the IDF default (enabled), so it is never written True here.
|
||||
wifi_disabled = net.ethernet and not net.wifi
|
||||
if wifi_disabled:
|
||||
set_idf_sdkconfig_default("CONFIG_ESP_WIFI_ENABLED", False)
|
||||
set_opt("CONFIG_ESP_WIFI_ENABLED", False)
|
||||
|
||||
# Software coexistence: enable when requested (the schema only allows it
|
||||
# alongside WiFi). Disable only in the Ethernet-without-WiFi case.
|
||||
if net.software_coexistence:
|
||||
set_idf_sdkconfig_default("CONFIG_SW_COEXIST_ENABLE", True)
|
||||
set_opt("CONFIG_SW_COEXIST_ENABLE", True)
|
||||
elif wifi_disabled:
|
||||
set_idf_sdkconfig_default("CONFIG_SW_COEXIST_ENABLE", False)
|
||||
set_opt("CONFIG_SW_COEXIST_ENABLE", False)
|
||||
|
||||
# SoftAP support: drop it when WiFi is used without AP mode (IDF only).
|
||||
if not is_arduino and net.wifi and not net.wifi_ap:
|
||||
set_idf_sdkconfig_default("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False)
|
||||
set_opt("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False)
|
||||
|
||||
# LWIP DHCP server: a WiFi-AP-mode / enable_lwip_dhcp_server concern (not
|
||||
# coexistence). Disable when WiFi has no AP (IDF) or the enable_lwip_dhcp_server
|
||||
@@ -2336,7 +2270,7 @@ async def _reconcile_network_sdkconfig() -> None:
|
||||
if (
|
||||
wifi_wants_dhcps_off or dhcp_server_disabled_by_option
|
||||
) and not arduino_eth_exclusion:
|
||||
set_idf_sdkconfig_default("CONFIG_LWIP_DHCPS", False)
|
||||
set_opt("CONFIG_LWIP_DHCPS", False)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
@@ -2361,24 +2295,29 @@ async def _reconcile_vfs_fatfs_sdkconfig(
|
||||
"""Reconcile VFS/FATFS sdkconfig flags after all require_*() calls; user sdkconfig_options win."""
|
||||
opts = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
|
||||
def set_opt(name: str, value: SdkconfigValueType) -> None:
|
||||
# User sdkconfig_options (applied during to_code) win.
|
||||
if name not in opts:
|
||||
add_idf_sdkconfig_option(name, value)
|
||||
|
||||
# USB Serial JTAG VFS needs termios (require_vfs_termios(), e.g. logger). ~1.8KB flash when off.
|
||||
if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False):
|
||||
set_idf_sdkconfig_default("CONFIG_VFS_SUPPORT_TERMIOS", True)
|
||||
set_opt("CONFIG_VFS_SUPPORT_TERMIOS", True)
|
||||
else:
|
||||
set_idf_sdkconfig_default("CONFIG_VFS_SUPPORT_TERMIOS", not disable_vfs_termios)
|
||||
set_opt("CONFIG_VFS_SUPPORT_TERMIOS", not disable_vfs_termios)
|
||||
|
||||
# VFS select is only needed for UART/eventfd fds (require_vfs_select(), e.g. openthread);
|
||||
# sockets use lwip_select() either way. ~2.7KB flash when off.
|
||||
if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False):
|
||||
set_idf_sdkconfig_default("CONFIG_VFS_SUPPORT_SELECT", True)
|
||||
set_opt("CONFIG_VFS_SUPPORT_SELECT", True)
|
||||
else:
|
||||
set_idf_sdkconfig_default("CONFIG_VFS_SUPPORT_SELECT", not disable_vfs_select)
|
||||
set_opt("CONFIG_VFS_SUPPORT_SELECT", not disable_vfs_select)
|
||||
|
||||
# Directory functions: opendir/readdir/mkdir etc. (require_vfs_dir()). ~0.5KB flash when off.
|
||||
if CORE.data.get(KEY_VFS_DIR_REQUIRED, False):
|
||||
set_idf_sdkconfig_default("CONFIG_VFS_SUPPORT_DIR", True)
|
||||
set_opt("CONFIG_VFS_SUPPORT_DIR", True)
|
||||
else:
|
||||
set_idf_sdkconfig_default("CONFIG_VFS_SUPPORT_DIR", not disable_vfs_dir)
|
||||
set_opt("CONFIG_VFS_SUPPORT_DIR", not disable_vfs_dir)
|
||||
|
||||
# FATFS (require_fatfs()): LFN + one volume per esp_vfs_fat mount. Defaults only;
|
||||
# sdkconfig_options override. FATFS_LONG_FILENAMES is a Kconfig choice -- if the user set
|
||||
@@ -2391,15 +2330,15 @@ async def _reconcile_vfs_fatfs_sdkconfig(
|
||||
user_picked_lfn = any(k in opts for k in lfn_keys)
|
||||
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
|
||||
if not user_picked_lfn:
|
||||
set_idf_sdkconfig_default("CONFIG_FATFS_LFN_NONE", False)
|
||||
set_idf_sdkconfig_default("CONFIG_FATFS_LFN_HEAP", True)
|
||||
set_idf_sdkconfig_default("CONFIG_FATFS_MAX_LFN", 255)
|
||||
set_idf_sdkconfig_default("CONFIG_FATFS_VOLUME_COUNT", 4)
|
||||
set_opt("CONFIG_FATFS_LFN_NONE", False)
|
||||
set_opt("CONFIG_FATFS_LFN_HEAP", True)
|
||||
set_opt("CONFIG_FATFS_MAX_LFN", 255)
|
||||
set_opt("CONFIG_FATFS_VOLUME_COUNT", 4)
|
||||
elif disable_fatfs:
|
||||
if not user_picked_lfn:
|
||||
set_idf_sdkconfig_default("CONFIG_FATFS_LFN_NONE", True)
|
||||
set_opt("CONFIG_FATFS_LFN_NONE", True)
|
||||
# Kconfig range is [1,10]; 0 gets clamped to the default.
|
||||
set_idf_sdkconfig_default("CONFIG_FATFS_VOLUME_COUNT", 1)
|
||||
set_opt("CONFIG_FATFS_VOLUME_COUNT", 1)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL - 1)
|
||||
@@ -2586,11 +2525,21 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
|
||||
cg.add_build_flag("-Wno-nonnull-compare")
|
||||
|
||||
if conf[CONF_ADVANCED].get(CONF_USE_FULL_CERTIFICATE_BUNDLE, False):
|
||||
require_full_certificate_bundle()
|
||||
# Use CMN (common CAs) bundle by default to save ~51KB flash
|
||||
# CMN covers CAs with >1% market share (~99% of websites)
|
||||
# Components needing uncommon CAs can call require_full_certificate_bundle()
|
||||
use_full_bundle = conf[CONF_ADVANCED].get(
|
||||
CONF_USE_FULL_CERTIFICATE_BUNDLE, False
|
||||
) or CORE.data[KEY_ESP32].get(KEY_FULL_CERT_BUNDLE, False)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL", use_full_bundle
|
||||
)
|
||||
if not use_full_bundle:
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
|
||||
|
||||
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
|
||||
add_idf_sdkconfig_option(
|
||||
@@ -2980,9 +2929,6 @@ async def to_code(config):
|
||||
# FINAL priority: runs after every network/coexistence request_*() call
|
||||
CORE.add_job(_reconcile_network_sdkconfig)
|
||||
|
||||
# FINAL priority: runs after every require_certificate_bundle() call
|
||||
CORE.add_job(_reconcile_certificate_bundle_sdkconfig)
|
||||
|
||||
# FINAL: require_*() calls can come from to_code at or below this priority, so an
|
||||
# inline read would be iteration-order-dependent; reconcile once after every job ran.
|
||||
CORE.add_job(
|
||||
@@ -3010,10 +2956,6 @@ async def to_code(config):
|
||||
|
||||
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
|
||||
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
|
||||
# A bundle forced on through sdkconfig_options is a request like any other,
|
||||
# so it still gets the CMN variant pinned.
|
||||
if conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE") == "y":
|
||||
require_certificate_bundle()
|
||||
|
||||
# Components from YAML are added in a separate coroutine with FINAL priority
|
||||
# Schedule it to run after all other components
|
||||
|
||||
@@ -27,7 +27,6 @@ KEY_REFRESH = "refresh"
|
||||
KEY_PATH = "path"
|
||||
KEY_SUBMODULES = "submodules"
|
||||
KEY_EXTRA_BUILD_FILES = "extra_build_files"
|
||||
KEY_CERT_BUNDLE = "cert_bundle"
|
||||
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
|
||||
KEY_NETWORK_SDKCONFIG = "network_sdkconfig"
|
||||
|
||||
|
||||
@@ -143,13 +143,6 @@ def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
|
||||
# BLE uses the airtime wifi does not claim.
|
||||
IDF_SCAN_WINDOW_FIX_VERSION = cv.Version(5, 5, 5)
|
||||
|
||||
# Above this the scanner holds the shared radio long enough that wifi drops
|
||||
# packets and connections on some access points (others cope fine, which is
|
||||
# why this is a warning and not an error); old proxy configs with 1100 ms
|
||||
# windows are a recurring cause of instability (esphome/esphome#18655). Only
|
||||
# wifi shares the radio; long windows are fine on ethernet builds.
|
||||
MAX_RECOMMENDED_WIFI_SCAN_WINDOW = TimePeriod(milliseconds=600)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrackerData:
|
||||
@@ -216,45 +209,6 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _warn_long_scan_window_with_wifi(config: ConfigType) -> ConfigType:
|
||||
"""Warn when the scan window is long enough to starve wifi.
|
||||
|
||||
Runs after _raise_defaulted_scan_window so it sees the final window.
|
||||
software_coexistence is only present when wifi is configured, so ethernet
|
||||
builds never warn: BLE has the radio to itself there. Presence is what
|
||||
matters, not the value; with the arbiter disabled a long window starves
|
||||
wifi outright.
|
||||
"""
|
||||
params = config[CONF_SCAN_PARAMETERS]
|
||||
window = params[CONF_WINDOW]
|
||||
if CONF_SOFTWARE_COEXISTENCE not in config:
|
||||
return config
|
||||
if window <= MAX_RECOMMENDED_WIFI_SCAN_WINDOW:
|
||||
return config
|
||||
if _get_data().scan_window_defaulted:
|
||||
# The window was raised to match the interval, so point at the key the
|
||||
# user actually set.
|
||||
_LOGGER.warning(
|
||||
"BLE scan interval of %s sets the scan window to the same value, "
|
||||
"which starves wifi on the same radio and can cause wifi disconnects "
|
||||
"depending on the access point; keep the interval at or below %s "
|
||||
"(for example interval: 320ms). Long windows are only a problem with "
|
||||
"wifi, they are fine on ethernet",
|
||||
params[CONF_INTERVAL],
|
||||
MAX_RECOMMENDED_WIFI_SCAN_WINDOW,
|
||||
)
|
||||
return config
|
||||
_LOGGER.warning(
|
||||
"BLE scan window of %s with wifi on the same radio starves wifi and "
|
||||
"can cause wifi disconnects depending on the access point; keep the "
|
||||
"window at or below %s (for example interval: 320ms, window: 300ms). "
|
||||
"Long windows are only a problem with wifi, they are fine on ethernet",
|
||||
window,
|
||||
MAX_RECOMMENDED_WIFI_SCAN_WINDOW,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
# 320 ms is the ESP-IDF reference scan interval; the shared schema also
|
||||
# tightens validation to the controller's 2.5 ms .. 10240 ms range and rejects
|
||||
# window/interval pairs that collapse to the same 0.625 ms unit count.
|
||||
@@ -317,7 +271,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
validate_max_connections_deprecated,
|
||||
_raise_defaulted_scan_window,
|
||||
_warn_long_scan_window_with_wifi,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import include_builtin_idf_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
|
||||
from esphome.types import ConfigType
|
||||
@@ -36,7 +35,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
|
||||
},
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on_esp32,
|
||||
_consume_camera_web_server_sockets,
|
||||
)
|
||||
|
||||
@@ -46,5 +44,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(server.set_port(config[CONF_PORT]))
|
||||
cg.add(server.set_mode(config[CONF_MODE]))
|
||||
await cg.register_component(server, config)
|
||||
# esp_http_server is excluded from IDF builds by default to save compile time
|
||||
include_builtin_idf_component("esp_http_server")
|
||||
|
||||
@@ -35,7 +35,7 @@ from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .boards import BOARDS, ESP8266_LD_SCRIPTS, board_ld_script
|
||||
from .boards import BOARDS, ESP8266_LD_SCRIPTS
|
||||
from .const import (
|
||||
CONF_EARLY_PIN_INIT,
|
||||
CONF_ENABLE_SERIAL,
|
||||
@@ -44,7 +44,6 @@ from .const import (
|
||||
KEY_BOARD,
|
||||
KEY_ESP8266,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_LDSCRIPT,
|
||||
KEY_PIN_INITIAL_STATES,
|
||||
KEY_SERIAL1_REQUIRED,
|
||||
KEY_SERIAL_REQUIRED,
|
||||
@@ -277,31 +276,6 @@ def check_rosetta() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _choose_ld_script(board: str, ver: cv.Version) -> str | None:
|
||||
"""The flash ld to pin for this board and core, or None for cores
|
||||
without ld-script support."""
|
||||
board_data = BOARDS[board]
|
||||
ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]]
|
||||
if ver <= cv.Version(2, 3, 0):
|
||||
# No ld script support
|
||||
return None
|
||||
if ver <= cv.Version(2, 4, 2):
|
||||
# Old ld script path; the modern per-board override names do not
|
||||
# exist in this core's SDK, so the override cannot be honored.
|
||||
# Substituting the size default would move _FS_end and the
|
||||
# preferences sector, wiping flash-backed state on flash.
|
||||
if KEY_LDSCRIPT in board_data:
|
||||
raise EsphomeError(
|
||||
f"Board {board} requires its {board_data[KEY_LDSCRIPT]} "
|
||||
f"flash layout, which Arduino core {ver} cannot honor; "
|
||||
"use a core newer than 2.4.2"
|
||||
)
|
||||
return ld_scripts[0]
|
||||
# A per-board override preserves a layout the board shipped with
|
||||
# (see d1_wroom_02 in boards.py)
|
||||
return board_ld_script(board_data)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.PLATFORM)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add(esp8266_ns.setup_preferences())
|
||||
@@ -423,7 +397,17 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
if config[CONF_BOARD] in BOARDS:
|
||||
ld_script = _choose_ld_script(config[CONF_BOARD], ver)
|
||||
flash_size = BOARDS[config[CONF_BOARD]][KEY_FLASH_SIZE]
|
||||
ld_scripts = ESP8266_LD_SCRIPTS[flash_size]
|
||||
|
||||
if ver <= cv.Version(2, 3, 0):
|
||||
# No ld script support
|
||||
ld_script = None
|
||||
elif ver <= cv.Version(2, 4, 2):
|
||||
# Old ld script path
|
||||
ld_script = ld_scripts[0]
|
||||
else:
|
||||
ld_script = ld_scripts[1]
|
||||
|
||||
if ld_script is not None:
|
||||
cg.add_platformio_option("board_build.ldscript", ld_script)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from .const import KEY_FLASH_SIZE, KEY_LDSCRIPT
|
||||
|
||||
FLASH_SIZE_1_MB = 2**20
|
||||
FLASH_SIZE_512_KB = FLASH_SIZE_1_MB // 2
|
||||
FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB
|
||||
@@ -166,8 +164,7 @@ ESP8266_BOARD_PINS = {
|
||||
}
|
||||
|
||||
"""
|
||||
BOARDS generate with (preserve per-board KEY_LDSCRIPT overrides such as
|
||||
d1_wroom_02; the recipe emits only name/flash_size):
|
||||
BOARDS generate with:
|
||||
|
||||
git clone https://github.com/platformio/platform-espressif8266
|
||||
for x in platform-espressif8266/boards/*.json; do
|
||||
@@ -185,19 +182,6 @@ for x in platform-espressif8266/boards/*.json; do
|
||||
done | sort
|
||||
"""
|
||||
|
||||
|
||||
def board_ld_script(board_data: dict) -> str:
|
||||
"""The modern (core > 2.4.2) flash linker script for a board: its
|
||||
shipped-layout override, else the size default (the no-FS layout).
|
||||
|
||||
Single source of truth for the PlatformIO pinning in __init__ and the
|
||||
native generator's fallback, so the per-board rule cannot drift.
|
||||
"""
|
||||
return board_data.get(
|
||||
KEY_LDSCRIPT, ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]][1]
|
||||
)
|
||||
|
||||
|
||||
BOARDS = {
|
||||
"agruminolemon": {
|
||||
"name": "Lifely Agrumino Lemon v4",
|
||||
@@ -215,15 +199,6 @@ BOARDS = {
|
||||
"name": "WeMos D1 mini Pro",
|
||||
"flash_size": FLASH_SIZE_16_MB,
|
||||
},
|
||||
"d1_wroom_02": {
|
||||
"name": "WeMos D1 ESP-WROOM-02",
|
||||
"flash_size": FLASH_SIZE_2_MB,
|
||||
# This board joined BOARDS after shipping with the manifest default
|
||||
# (64 KB filesystem region); the flash-size default (2m.ld) would
|
||||
# move _FS_end and with it the preferences sector, wiping existing
|
||||
# devices' flash-backed state on update.
|
||||
KEY_LDSCRIPT: "eagle.flash.2m64.ld",
|
||||
},
|
||||
"d1": {
|
||||
"name": "WEMOS D1 R1",
|
||||
"flash_size": FLASH_SIZE_4_MB,
|
||||
@@ -385,112 +360,3 @@ BOARDS = {
|
||||
"flash_size": FLASH_SIZE_4_MB,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Per-board variant dir + identity defines from platform-espressif8266 4.x
|
||||
# build.extra_flags; the shared -DESP8266/-DARDUINO_ARCH_ESP8266 are added
|
||||
# by the generator.
|
||||
#
|
||||
# Regenerate ESP8266_BOARD_BUILD with (v4.2.1 is the platform version the
|
||||
# native toolchain mirrors; regenerate against the tag when bumping it):
|
||||
#
|
||||
# git clone -b v4.2.1 https://github.com/platformio/platform-espressif8266
|
||||
# python3 - <<'EOF'
|
||||
# import json, glob, os
|
||||
# for f in sorted(glob.glob("platform-espressif8266/boards/*.json")):
|
||||
# b = json.load(open(f))["build"]
|
||||
# extra = b["extra_flags"]
|
||||
# extra = extra.split() if isinstance(extra, str) else extra
|
||||
# defines = [
|
||||
# e[2:] for e in extra if e not in ("-DESP8266", "-DARDUINO_ARCH_ESP8266")
|
||||
# ]
|
||||
# entries = ", ".join(f'"{d}"' for d in defines) + ("," if len(defines) == 1 else "")
|
||||
# board = os.path.splitext(os.path.basename(f))[0]
|
||||
# print(f' "{board}": {{"variant": "{b["variant"]}", "defines": ({entries})}},')
|
||||
# EOF
|
||||
ESP8266_BOARD_BUILD = {
|
||||
"agruminolemon": {
|
||||
"variant": "agruminolemonv4",
|
||||
"defines": ("ARDUINO_ESP8266_AGRUMINO_LEMON_V4",),
|
||||
},
|
||||
"d1": {"variant": "d1", "defines": ("ARDUINO_ESP8266_WEMOS_D1R1",)},
|
||||
"d1_mini": {"variant": "d1_mini", "defines": ("ARDUINO_ESP8266_WEMOS_D1MINI",)},
|
||||
"d1_mini_lite": {
|
||||
"variant": "d1_mini",
|
||||
"defines": ("ARDUINO_ESP8266_WEMOS_D1MINILITE",),
|
||||
},
|
||||
"d1_mini_pro": {
|
||||
"variant": "d1_mini",
|
||||
"defines": ("ARDUINO_ESP8266_WEMOS_D1MINIPRO",),
|
||||
},
|
||||
"d1_wroom_02": {
|
||||
"variant": "d1_mini",
|
||||
"defines": ("ARDUINO_ESP8266_WEMOS_D1WROOM02",),
|
||||
},
|
||||
"eduinowifi": {
|
||||
"variant": "eduinowifi",
|
||||
"defines": ("ARDUINO_ESP8266_SCHIRMILABS_EDUINO_WIFI",),
|
||||
},
|
||||
"esp01": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP01",)},
|
||||
"esp01_1m": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP01",)},
|
||||
"esp07": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP07",)},
|
||||
"esp07s": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_ESP07",)},
|
||||
"esp12e": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_ESP12",)},
|
||||
"esp210": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP210",)},
|
||||
"esp8285": {"variant": "esp8285", "defines": ("ARDUINO_ESP8266_ESP01",)},
|
||||
"esp_wroom_02": {
|
||||
"variant": "nodemcu",
|
||||
"defines": ("ARDUINO_ESP8266_ESP_WROOM_02",),
|
||||
},
|
||||
"espduino": {"variant": "ESPDuino", "defines": ("ARDUINO_ESP8266_ESP13",)},
|
||||
"espectro": {"variant": "espectro", "defines": ("ARDUINO_ESP8266_ESPECTRO_CORE",)},
|
||||
"espino": {"variant": "espino", "defines": ("ARDUINO_ESP8266_ESP12",)},
|
||||
"espinotee": {"variant": "espinotee", "defines": ("ARDUINO_ESP8266_ESP13",)},
|
||||
"espmxdevkit": {
|
||||
"variant": "esp8285",
|
||||
"defines": ("ARDUINO_ESP8266_ESP01", "LED_BUILTIN=16"),
|
||||
},
|
||||
"espresso_lite_v1": {
|
||||
"variant": "espresso_lite_v1",
|
||||
"defines": ("ARDUINO_ESP8266_ESPRESSO_LITE_V1",),
|
||||
},
|
||||
"espresso_lite_v2": {
|
||||
"variant": "espresso_lite_v2",
|
||||
"defines": ("ARDUINO_ESP8266_ESPRESSO_LITE_V2",),
|
||||
},
|
||||
"gen4iod": {"variant": "generic", "defines": ("ARDUINO_GEN4_IOD",)},
|
||||
"heltec_wifi_kit_8": {
|
||||
"variant": "wifi_kit_8",
|
||||
"defines": ("ARDUINO_wifi_kit_8",),
|
||||
},
|
||||
"huzzah": {"variant": "adafruit", "defines": ("ARDUINO_ESP8266_ADAFRUIT_HUZZAH",)},
|
||||
"inventone": {"variant": "inventone", "defines": ("ARDUINO_ESP8266_INVENT_ONE",)},
|
||||
"modwifi": {"variant": "generic", "defines": ("ARDUINO_MOD_WIFI_ESP8266",)},
|
||||
"nodemcu": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_NODEMCU",)},
|
||||
"nodemcuv2": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_NODEMCU_ESP12E",)},
|
||||
"oak": {"variant": "oak", "defines": ("ARDUINO_ESP8266_OAK",)},
|
||||
"phoenix_v1": {
|
||||
"variant": "phoenix_v1",
|
||||
"defines": ("ARDUINO_ESP8266_PHOENIX_V1",),
|
||||
},
|
||||
"phoenix_v2": {
|
||||
"variant": "phoenix_v2",
|
||||
"defines": ("ARDUINO_ESP8266_PHOENIX_V2",),
|
||||
},
|
||||
"sonoff_basic": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_BASIC",)},
|
||||
"sonoff_s20": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_S20",)},
|
||||
"sonoff_sv": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_SV",)},
|
||||
"sonoff_th": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_TH",)},
|
||||
"sparkfunBlynk": {"variant": "thing", "defines": ("ARDUINO_ESP8266_THING",)},
|
||||
"thing": {"variant": "thing", "defines": ("ARDUINO_ESP8266_THING",)},
|
||||
"thingdev": {"variant": "thing", "defines": ("ARDUINO_ESP8266_THING_DEV",)},
|
||||
"wifi_slot": {"variant": "wifi_slot", "defines": ("ARDUINO_AMPERKA_WIFI_SLOT",)},
|
||||
"wifiduino": {"variant": "wifiduino", "defines": ("ARDUINO_WIFIDUINO_ESP8266",)},
|
||||
"wifinfo": {"variant": "wifinfo", "defines": ("ARDUINO_WIFINFO",)},
|
||||
"wio_link": {"variant": "wiolink", "defines": ("ARDUINO_ESP8266_WIO_LINK",)},
|
||||
"wio_node": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_ESP_WROOM_02",)},
|
||||
"xinabox_cw01": {
|
||||
"variant": "xinabox",
|
||||
"defines": ("ARDUINO_ESP8266_XINABOX_CW01",),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
"""Linker-script surgery shared with the native (PlatformIO-free) toolchain.
|
||||
|
||||
These mirror the PlatformIO extra scripts in this directory
|
||||
(``relocate_ratetable.py.script`` and ``testing_mode.py.script``), which run
|
||||
inside SCons and must stay self-contained. The native build generator applies
|
||||
the same patches to the linker scripts it generates, so the logic lives here
|
||||
as plain functions. Keep both in sync when changing either.
|
||||
``segment_length`` is native-toolchain-only and has no script twin.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Collection
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
# Move the NONOS SDK wifi rate tables from flash to DRAM; see
|
||||
# relocate_ratetable.py.script for the full background (NONOS SDK issue 320).
|
||||
RATETABLE_RULE = "*libnet80211.a:ieee80211_phy.o(.irom.text .irom.text.*)"
|
||||
_RATETABLE_COMMENT = (
|
||||
"/* ESPHome: wifi rate tables must live in DRAM, see NONOS SDK issue 320 */"
|
||||
)
|
||||
# Match the whole line: "_data_start" is also a substring of the
|
||||
# "_dport0_data_start" line in the earlier .dport0.data section
|
||||
_RATETABLE_ANCHOR = re.compile(r"^\s*_data_start = ABSOLUTE\(\.\);", re.MULTILINE)
|
||||
|
||||
# Memory sizes for testing mode (allow larger builds for CI component grouping)
|
||||
TESTING_IRAM_SIZE = "0x200000" # 2MB
|
||||
TESTING_DRAM_SIZE = "0x200000" # 2MB
|
||||
TESTING_FLASH_SIZE = "0x2000000" # 32MB
|
||||
|
||||
|
||||
def relocate_ratetable(content: str) -> str:
|
||||
"""Insert the rate-table DRAM rule into a generated common linker script."""
|
||||
if RATETABLE_RULE in content:
|
||||
return content
|
||||
match = _RATETABLE_ANCHOR.search(content)
|
||||
if match is None:
|
||||
raise RuntimeError(
|
||||
"'_data_start' anchor not found in the generated linker script; "
|
||||
"cannot apply wifi rate table DRAM relocation "
|
||||
"(has the Arduino core linker script changed?)"
|
||||
)
|
||||
insert_pos = match.end()
|
||||
return (
|
||||
content[:insert_pos]
|
||||
+ f"\n {_RATETABLE_COMMENT}"
|
||||
+ f"\n {RATETABLE_RULE}"
|
||||
+ content[insert_pos:]
|
||||
)
|
||||
|
||||
|
||||
_TESTING_SEGMENT_SIZES = {
|
||||
"iram1_0_seg": TESTING_IRAM_SIZE,
|
||||
"dram0_0_seg": TESTING_DRAM_SIZE,
|
||||
"irom0_0_seg": TESTING_FLASH_SIZE,
|
||||
}
|
||||
|
||||
|
||||
def _segment_line_re(segment_name: str) -> re.Pattern[str]:
|
||||
"""The MEMORY line for one segment: ``<seg> : org = 0x..., len = 0x...``.
|
||||
|
||||
Anchored to the start of the line so a name never matches inside a
|
||||
longer one (``ram0_0_seg`` must not read ``dram0_0_seg``). The size
|
||||
group stops at the hex digits, leaving any ``ul`` suffix (from the
|
||||
preprocessed ``MMU_IRAM_SIZE``) in place.
|
||||
"""
|
||||
return re.compile(
|
||||
rf"(^[ \t]*{re.escape(segment_name)}"
|
||||
r"\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)"
|
||||
r"(0x[0-9a-fA-F]+)",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def apply_testing_memory_patches(content: str, segments: Collection[str]) -> str:
|
||||
"""Enlarge the named memory segments so grouped CI test builds can link.
|
||||
|
||||
Each caller passes the segments its linker script defines: the
|
||||
generated common ld carries ``iram1_0_seg``; the flash ld carries
|
||||
``dram0_0_seg`` and ``irom0_0_seg``. A segment that fails to match
|
||||
raises, since a silently kept real memory limit would fail grouped
|
||||
builds far from the cause.
|
||||
"""
|
||||
for segment in _TESTING_SEGMENT_SIZES:
|
||||
if segment not in segments and _segment_line_re(segment).search(content):
|
||||
raise RuntimeError(
|
||||
f"Testing-mode segment {segment} is present in the linker "
|
||||
"script but was not selected for patching"
|
||||
)
|
||||
for segment in segments:
|
||||
if segment not in _TESTING_SEGMENT_SIZES:
|
||||
raise RuntimeError(f"Unknown testing-mode segment {segment!r}")
|
||||
content, count = _segment_line_re(segment).subn(
|
||||
rf"\g<1>{_TESTING_SEGMENT_SIZES[segment]}", content
|
||||
)
|
||||
if count == 0:
|
||||
raise RuntimeError(
|
||||
f"Testing-mode memory patch failed: segment {segment} "
|
||||
"not found (has the Arduino core linker script changed?)"
|
||||
)
|
||||
return content
|
||||
|
||||
|
||||
def segment_length(content: str, segment_name: str) -> int | None:
|
||||
"""Read a memory segment's length from linker script content.
|
||||
|
||||
Returns None for an absent segment OR an unparsable line; callers must
|
||||
treat None as "no usable budget" and warn (as the Flash summary does),
|
||||
never as "no limit".
|
||||
"""
|
||||
match = _segment_line_re(segment_name).search(content)
|
||||
return int(match.group(2), 16) if match else None
|
||||
|
||||
|
||||
def surgery_fingerprint() -> str:
|
||||
"""Hash of this module's source; linker-script caches include it so an
|
||||
edit here invalidates them."""
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
source = inspect.getsource(sys.modules[__name__])
|
||||
return hashlib.sha256(source.encode()).hexdigest()
|
||||
@@ -15,11 +15,6 @@ CONF_ENABLE_SERIAL1 = "enable_serial1"
|
||||
KEY_WAVEFORM_REQUIRED = "waveform_required"
|
||||
KEY_SERIAL_REQUIRED = "serial_required"
|
||||
KEY_SERIAL1_REQUIRED = "serial1_required"
|
||||
# Set for the native (non-PlatformIO) toolchain's build generator
|
||||
KEY_FLASH_MODE = "flash_mode"
|
||||
KEY_SCANF_FLOAT = "scanf_float"
|
||||
# Per-board flash-layout override consumed by board_ld_script()
|
||||
KEY_LDSCRIPT = "ldscript"
|
||||
|
||||
# esp8266 namespace is already defined by arduino, manually prefix esphome
|
||||
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
|
||||
|
||||
@@ -4,7 +4,6 @@ import logging
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
from esphome.components.network import (
|
||||
add_use_address,
|
||||
get_network_priority,
|
||||
@@ -40,7 +39,6 @@ from esphome.const import (
|
||||
CONF_POLLING_INTERVAL,
|
||||
CONF_RESET_PIN,
|
||||
CONF_SPI,
|
||||
CONF_SPI_ID,
|
||||
CONF_STATIC_IP,
|
||||
CONF_SUBNET,
|
||||
CONF_TYPE,
|
||||
@@ -265,42 +263,10 @@ def _is_framework_spi_polling_mode_supported() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# Options that come from the referenced spi bus when spi_id is set
|
||||
_SPI_BUS_PROVIDED_OPTIONS = (
|
||||
CONF_CLK_PIN,
|
||||
CONF_MOSI_PIN,
|
||||
CONF_MISO_PIN,
|
||||
CONF_INTERFACE,
|
||||
)
|
||||
|
||||
|
||||
def _validate_spi_bus(config: ConfigType) -> ConfigType:
|
||||
"""Cross-validate spi_id against the options the referenced bus provides."""
|
||||
if CONF_SPI_ID in config:
|
||||
for key in _SPI_BUS_PROVIDED_OPTIONS:
|
||||
if key in config:
|
||||
raise cv.Invalid(
|
||||
f"'{key}' cannot be used together with '{CONF_SPI_ID}'; "
|
||||
f"it comes from the referenced 'spi:' bus.",
|
||||
path=[key],
|
||||
)
|
||||
else:
|
||||
for key in (CONF_CLK_PIN, CONF_MOSI_PIN, CONF_MISO_PIN):
|
||||
if key not in config:
|
||||
raise cv.Invalid(
|
||||
f"'{key}' is a required option when '{CONF_SPI_ID}' is not set.",
|
||||
path=[key],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _validate_spi_interface(config: ConfigType) -> ConfigType:
|
||||
"""Set default SPI interface or validate user choice against the variant."""
|
||||
if not CORE.is_esp32:
|
||||
return config
|
||||
if CONF_SPI_ID in config:
|
||||
# The interface comes from the referenced spi bus; don't set a default.
|
||||
return config
|
||||
from esphome.components.esp32 import VARIANT_ESP32, get_esp32_variant
|
||||
from esphome.components.spi import get_hw_interface_list
|
||||
|
||||
@@ -485,14 +451,9 @@ def _spi_schema(default_clock: str = "26.67MHz", max_clock: int = int(80e6)) ->
|
||||
BASE_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
# clk/mosi/miso are required unless spi_id is set; enforced
|
||||
# by _validate_spi_bus below.
|
||||
cv.Optional(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_MISO_PIN): pins.internal_gpio_input_pin_number,
|
||||
cv.Optional(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_SPI_ID): cv.All(
|
||||
cv.only_on_esp32, cv.use_id(spi.SPIComponent)
|
||||
),
|
||||
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number,
|
||||
cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(
|
||||
CONF_INTERRUPT_PIN
|
||||
@@ -517,7 +478,6 @@ def _spi_schema(default_clock: str = "26.67MHz", max_clock: int = int(80e6)) ->
|
||||
),
|
||||
),
|
||||
cv.only_on([Platform.ESP32, Platform.RP2]),
|
||||
_validate_spi_bus,
|
||||
_validate_spi_interface,
|
||||
)
|
||||
|
||||
@@ -569,30 +529,6 @@ def _final_validate_spi(config: ConfigType) -> None:
|
||||
return
|
||||
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
|
||||
|
||||
if CONF_SPI_ID in config:
|
||||
# Sharing the bus: the standard spi device schema enforces that the
|
||||
# referenced bus declares both data lines. The IDF ethernet drivers
|
||||
# additionally need a hardware host, which shows as an interface index
|
||||
# on the validated bus config.
|
||||
spi.final_validate_device_schema(
|
||||
"ethernet", require_mosi=True, require_miso=True
|
||||
)(config)
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_SPI_ID): fv.id_declaration_match_schema(
|
||||
{
|
||||
cv.Required(
|
||||
CONF_INTERFACE_INDEX,
|
||||
msg="Component ethernet requires this spi bus to use "
|
||||
"a hardware interface",
|
||||
): cv.valid
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)(config)
|
||||
return
|
||||
|
||||
if spi_configs := fv.full_config.get().get(CONF_SPI):
|
||||
# get_spi_interface() returns strings like "SPI2_HOST"
|
||||
spi_host = f"{config[CONF_INTERFACE].upper()}_HOST"
|
||||
@@ -689,15 +625,9 @@ async def _to_code_esp32(var: cg.MockObj, config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
|
||||
if (spi_id := config.get(CONF_SPI_ID)) is not None:
|
||||
# Pins and host come from the shared spi bus.
|
||||
spi_parent = await cg.get_variable(spi_id)
|
||||
cg.add(var.set_spi_parent(spi_parent))
|
||||
else:
|
||||
cg.add(var.set_clk_pin(config[CONF_CLK_PIN]))
|
||||
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
|
||||
cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN]))
|
||||
cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]]))
|
||||
cg.add(var.set_clk_pin(config[CONF_CLK_PIN]))
|
||||
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
|
||||
cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN]))
|
||||
cg.add(var.set_cs_pin(config[CONF_CS_PIN]))
|
||||
if CONF_INTERRUPT_PIN in config:
|
||||
cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN]))
|
||||
@@ -711,6 +641,7 @@ async def _to_code_esp32(var: cg.MockObj, config: ConfigType) -> None:
|
||||
|
||||
cg.add_define("USE_ETHERNET_SPI")
|
||||
|
||||
cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]]))
|
||||
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
|
||||
# CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0
|
||||
# Types that are never built into IDF ship no Kconfig option at all
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
#include "esp_eth.h"
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
#include "hal/spi_types.h"
|
||||
#ifdef USE_SPI
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#endif
|
||||
#endif
|
||||
#include "esp_eth_mac.h"
|
||||
#include "esp_eth_mac_esp.h"
|
||||
@@ -179,9 +176,6 @@ class EthernetComponent final : public Component {
|
||||
void set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
|
||||
void set_interface(spi_host_device_t interface) { this->interface_ = interface; }
|
||||
#ifdef USE_SPI
|
||||
void set_spi_parent(spi::SPIComponent *parent) { this->spi_parent_ = parent; }
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
|
||||
#endif
|
||||
@@ -264,11 +258,6 @@ class EthernetComponent final : public Component {
|
||||
int phy_addr_spi_{-1};
|
||||
int clock_speed_;
|
||||
spi_host_device_t interface_{SPI2_HOST};
|
||||
#ifdef USE_SPI
|
||||
// When set, the SPI bus is owned and initialized by this spi component
|
||||
// and the ethernet chip only adds a device to it.
|
||||
spi::SPIComponent *spi_parent_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
uint32_t polling_interval_{0};
|
||||
#endif
|
||||
|
||||
@@ -59,9 +59,6 @@
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/spi_master.h>
|
||||
#ifdef USE_SPI
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace esphome::ethernet {
|
||||
@@ -171,34 +168,25 @@ void EthernetComponent::ethernet_lazy_init_() {
|
||||
// Install GPIO ISR handler to be able to service SPI Eth modules interrupts
|
||||
gpio_install_isr_service(0);
|
||||
|
||||
spi_host_device_t host;
|
||||
#ifdef USE_SPI
|
||||
if (this->spi_parent_ != nullptr) {
|
||||
// The bus is owned and already initialized by the spi component; share its host.
|
||||
host = this->spi_parent_->get_interface();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
spi_bus_config_t buscfg = {
|
||||
.mosi_io_num = this->mosi_pin_,
|
||||
.miso_io_num = this->miso_pin_,
|
||||
.sclk_io_num = this->clk_pin_,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.data4_io_num = -1,
|
||||
.data5_io_num = -1,
|
||||
.data6_io_num = -1,
|
||||
.data7_io_num = -1,
|
||||
.max_transfer_sz = 0,
|
||||
.flags = 0,
|
||||
.intr_flags = 0,
|
||||
};
|
||||
spi_bus_config_t buscfg = {
|
||||
.mosi_io_num = this->mosi_pin_,
|
||||
.miso_io_num = this->miso_pin_,
|
||||
.sclk_io_num = this->clk_pin_,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.data4_io_num = -1,
|
||||
.data5_io_num = -1,
|
||||
.data6_io_num = -1,
|
||||
.data7_io_num = -1,
|
||||
.max_transfer_sz = 0,
|
||||
.flags = 0,
|
||||
.intr_flags = 0,
|
||||
};
|
||||
|
||||
host = this->interface_;
|
||||
auto host = this->interface_;
|
||||
|
||||
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
|
||||
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
|
||||
}
|
||||
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
|
||||
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
|
||||
#endif
|
||||
// Network interface setup handled by network component
|
||||
|
||||
@@ -587,25 +575,17 @@ void EthernetComponent::dump_config() {
|
||||
YESNO(this->is_connected()));
|
||||
this->dump_connect_params_();
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
#ifdef USE_SPI
|
||||
if (this->spi_parent_ != nullptr) {
|
||||
// Pins and interface come from the shared spi bus; only CS is ours.
|
||||
ESP_LOGCONFIG(TAG, " CS Pin: %u", this->cs_pin_);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" CLK Pin: %u\n"
|
||||
" MISO Pin: %u\n"
|
||||
" MOSI Pin: %u\n"
|
||||
" CS Pin: %u",
|
||||
this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_);
|
||||
const char *spi_interface = "spi3";
|
||||
if (this->interface_ == SPI2_HOST) {
|
||||
spi_interface = "spi2";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Interface: %s", spi_interface);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" CLK Pin: %u\n"
|
||||
" MISO Pin: %u\n"
|
||||
" MOSI Pin: %u\n"
|
||||
" CS Pin: %u",
|
||||
this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_);
|
||||
const char *spi_interface = "spi3";
|
||||
if (this->interface_ == SPI2_HOST) {
|
||||
spi_interface = "spi2";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Interface: %s", spi_interface);
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
if (this->polling_interval_ != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_);
|
||||
|
||||
@@ -55,11 +55,8 @@ int HOT IRAM_ATTR GPIOOneWireBus::reset_int() {
|
||||
delayMicroseconds(1);
|
||||
}
|
||||
|
||||
// delay J: finish the 480us slot, but never spin if it already elapsed
|
||||
// (unsigned wrap here would busy-wait for minutes with interrupts off)
|
||||
uint32_t elapsed = micros() - start;
|
||||
if (elapsed < 480)
|
||||
delayMicroseconds(480 - elapsed);
|
||||
// delay J
|
||||
delayMicroseconds(start + 480 - micros());
|
||||
this->pin_.digital_write(true);
|
||||
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
|
||||
return r ? 1 : 0;
|
||||
|
||||
@@ -196,7 +196,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
# framework:
|
||||
# advanced:
|
||||
# use_full_certificate_bundle: true
|
||||
esp32.require_certificate_bundle()
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True
|
||||
)
|
||||
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_TLS_INSECURE",
|
||||
|
||||
@@ -14,7 +14,6 @@ void KeyCollector::loop() {
|
||||
}
|
||||
|
||||
void KeyCollector::dump_config() {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_CONFIG
|
||||
ESP_LOGCONFIG(TAG, "Key Collector:");
|
||||
if (this->min_length_ > 0)
|
||||
ESP_LOGCONFIG(TAG, " min length: %d", this->min_length_);
|
||||
@@ -36,7 +35,6 @@ void KeyCollector::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " allowed keys '%s'", this->allowed_keys_.c_str());
|
||||
if (this->timeout_ > 0)
|
||||
ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void KeyCollector::add_provider(key_provider::KeyProvider *provider) {
|
||||
|
||||
@@ -57,6 +57,7 @@ from .defines import (
|
||||
CONF_ALIGN_TO_LAMBDA_ID,
|
||||
CONF_ANIMATIONS,
|
||||
LOGGER,
|
||||
add_lv_use,
|
||||
get_focused_widgets,
|
||||
get_lv_images_used,
|
||||
get_refreshed_widgets,
|
||||
@@ -73,6 +74,7 @@ from .keypads import KEYPADS_CONFIG, keypads_to_code
|
||||
from .lv_validation import lv_bool
|
||||
from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static
|
||||
from .schemas import (
|
||||
BASE_PROPS,
|
||||
DISP_BG_SCHEMA,
|
||||
FULL_STYLE_SCHEMA,
|
||||
SET_STATE_SCHEMA,
|
||||
@@ -81,7 +83,6 @@ from .schemas import (
|
||||
STYLE_SCHEMA,
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
apply_style_driven_defines,
|
||||
container_schema,
|
||||
container_schema_value,
|
||||
theme_schema,
|
||||
@@ -107,6 +108,7 @@ from .widgets import (
|
||||
get_screen_active,
|
||||
set_obj_properties,
|
||||
)
|
||||
from .widgets.img import CONF_IMAGE
|
||||
|
||||
# Import only what we actually use directly in this file
|
||||
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
|
||||
@@ -453,15 +455,6 @@ async def to_code(configs):
|
||||
# Mark all widgets as completed so awaiters of ``wait_for_widgets`` proceed.
|
||||
set_widgets_completed(True)
|
||||
async with LvContext():
|
||||
# Local import: lv_list imports meter, which imports obj_spec/set_obj_properties
|
||||
# from this module's own namespace - a top-level import here would be circular.
|
||||
from .widgets.lv_list import finish_list_triggers
|
||||
|
||||
# Must run before generate_triggers(): that's what actually processes other
|
||||
# widgets' on_click etc. automations, which can include lvgl.list.add/remove/
|
||||
# clear actions that fire a list's on_add/on_remove triggers - those need to
|
||||
# already exist by then, not still be pending.
|
||||
await finish_list_triggers()
|
||||
await generate_triggers()
|
||||
await generate_align_tos(configs[0])
|
||||
for config in configs:
|
||||
@@ -488,16 +481,34 @@ async def to_code(configs):
|
||||
|
||||
# This must be done after all widgets are created
|
||||
styles_used = df.get_styles_used()
|
||||
apply_style_driven_defines(styles_used)
|
||||
if any(BASE_PROPS.get(x) is lvalid.lv_image for x in styles_used):
|
||||
add_lv_use(CONF_IMAGE)
|
||||
for use in df.get_lv_uses():
|
||||
df.add_define(f"LV_USE_{use.upper()}")
|
||||
cg.add_define(f"USE_LVGL_{use.upper()}")
|
||||
|
||||
if {
|
||||
"transform_rotation",
|
||||
"transform_scale",
|
||||
"transform_scale_x",
|
||||
"transform_scale_y",
|
||||
} & styles_used:
|
||||
df.add_define("LV_COLOR_SCREEN_TRANSP", "1")
|
||||
|
||||
if configs[0].get(df.CONF_THEME, {}).get(df.CONF_DARK_MODE):
|
||||
df.add_define("LV_THEME_DEFAULT_DARK", "1")
|
||||
|
||||
# Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending
|
||||
lv_image_formats = {"RGB565", "ARGB8888"}
|
||||
if {
|
||||
"drop_shadow_color",
|
||||
"drop_shadow_offset_x",
|
||||
"drop_shadow_offset_y",
|
||||
"drop_shadow_opa",
|
||||
"drop_shadow_quality",
|
||||
"drop_shadow_radius",
|
||||
} & styles_used:
|
||||
lv_image_formats.add("A8")
|
||||
|
||||
for image_id in get_lv_images_used():
|
||||
await cg.get_variable(image_id)
|
||||
|
||||
@@ -416,7 +416,7 @@ async def obj_set_z_index_to_code(config, action_id, template_arg, args):
|
||||
widget.obj, literal(f"{lv_expr.obj_get_index(widget.obj)} + 1")
|
||||
)
|
||||
elif position == "DOWN":
|
||||
with LvConditional(literal(f"{lv_expr.obj_get_index(widget.obj)} > 0")):
|
||||
with LvConditional(f"{lv_expr.obj_get_index(widget.obj)} > 0"):
|
||||
lv_obj.move_to_index(
|
||||
widget.obj, literal(f"{lv_expr.obj_get_index(widget.obj)} - 1")
|
||||
)
|
||||
|
||||
@@ -585,21 +585,6 @@ FLEX_FLOWS = LvConstant(
|
||||
"COLUMN_WRAP_REVERSE",
|
||||
)
|
||||
|
||||
TRANSFORM_STYLE_PROPS = frozenset(
|
||||
{"transform_rotation", "transform_scale", "transform_scale_x", "transform_scale_y"}
|
||||
)
|
||||
|
||||
DROP_SHADOW_STYLE_PROPS = frozenset(
|
||||
{
|
||||
"drop_shadow_color",
|
||||
"drop_shadow_offset_x",
|
||||
"drop_shadow_offset_y",
|
||||
"drop_shadow_opa",
|
||||
"drop_shadow_quality",
|
||||
"drop_shadow_radius",
|
||||
}
|
||||
)
|
||||
|
||||
OBJ_FLAGS = (
|
||||
"hidden",
|
||||
"clickable",
|
||||
|
||||
@@ -242,7 +242,7 @@ class LocalVariable(MockObj):
|
||||
self.base.type, self.modifier, self.base.id
|
||||
)
|
||||
)
|
||||
return MockObj(self.base, "->" if self.modifier == "*" else ".")
|
||||
return MockObj(self.base)
|
||||
|
||||
def __exit__(self, *args):
|
||||
CodeContext.end_block()
|
||||
@@ -283,15 +283,7 @@ class MockLv:
|
||||
|
||||
class LvConditional:
|
||||
def __init__(self, condition):
|
||||
# Condition is embedded directly into a raw `if (...)` statement below, rather than
|
||||
# going through the argument-list machinery (ExpressionList) that would otherwise
|
||||
# convert a native Python value (e.g. a plain bool) to a proper Expression.
|
||||
if isinstance(condition, str):
|
||||
raise ValueError(
|
||||
"LvConditional condition must not be a raw str; wrap it in literal() "
|
||||
"if a string literal condition is really intended"
|
||||
)
|
||||
self.condition = cg.safe_exp(condition) if condition is not None else None
|
||||
self.condition = condition
|
||||
|
||||
def __enter__(self):
|
||||
if self.condition is not None:
|
||||
@@ -311,35 +303,6 @@ class LvConditional:
|
||||
CodeContext.code_context.indent()
|
||||
|
||||
|
||||
class LvCountdown:
|
||||
"""
|
||||
Emits a C++ `for` loop that counts an int variable down from `count - 1` to `0` inclusive.
|
||||
Used to iterate over a widget's children in reverse, e.g. to fire a trigger once per child
|
||||
before they're all removed.
|
||||
"""
|
||||
|
||||
def __init__(self, var_name: str, count):
|
||||
self.var_name = var_name
|
||||
self.count = count
|
||||
|
||||
def __enter__(self):
|
||||
# Cast explicitly rather than relying on `count`'s (typically unsigned) type to wrap
|
||||
# and then narrow back to a negative int when count is 0 -- true in practice on every
|
||||
# toolchain ESPHome targets, but not worth leaning on.
|
||||
CodeContext.append(
|
||||
RawStatement(
|
||||
f"for (int {self.var_name} = (int) ({self.count}) - 1; {self.var_name} >= 0; "
|
||||
f"{self.var_name}--) {{"
|
||||
)
|
||||
)
|
||||
CodeContext.code_context.indent()
|
||||
return literal(self.var_name)
|
||||
|
||||
def __exit__(self, *args):
|
||||
CodeContext.code_context.detent()
|
||||
CodeContext.append(RawStatement("}"))
|
||||
|
||||
|
||||
class ReturnStatement(ExpressionStatement):
|
||||
def __str__(self):
|
||||
return f"return {self.expression};"
|
||||
|
||||
@@ -208,21 +208,21 @@ void LvglComponent::esphome_lvgl_init() {
|
||||
lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id());
|
||||
}
|
||||
|
||||
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event, void *user_data) {
|
||||
lv_obj_add_event_cb(obj, callback, event, user_data);
|
||||
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
|
||||
lv_obj_add_event_cb(obj, callback, event, nullptr);
|
||||
}
|
||||
|
||||
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
|
||||
lv_event_code_t event2, void *user_data) {
|
||||
add_event_cb(obj, callback, event1, user_data);
|
||||
add_event_cb(obj, callback, event2, user_data);
|
||||
lv_event_code_t event2) {
|
||||
add_event_cb(obj, callback, event1);
|
||||
add_event_cb(obj, callback, event2);
|
||||
}
|
||||
|
||||
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
|
||||
lv_event_code_t event2, lv_event_code_t event3, void *user_data) {
|
||||
add_event_cb(obj, callback, event1, user_data);
|
||||
add_event_cb(obj, callback, event2, user_data);
|
||||
add_event_cb(obj, callback, event3, user_data);
|
||||
lv_event_code_t event2, lv_event_code_t event3) {
|
||||
add_event_cb(obj, callback, event1);
|
||||
add_event_cb(obj, callback, event2);
|
||||
add_event_cb(obj, callback, event3);
|
||||
}
|
||||
|
||||
void LvglComponent::add_page(LvPageType *page) {
|
||||
@@ -963,25 +963,6 @@ lv_obj_t *lv_container_create(lv_obj_t *parent) {
|
||||
lv_obj_class_init_obj(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
#ifdef USE_LVGL_LIST
|
||||
int lv_list_get_row_index(lv_obj_t *list, lv_obj_t *child) {
|
||||
for (lv_obj_t *obj = child; obj != nullptr; obj = lv_obj_get_parent(obj)) {
|
||||
if (lv_obj_get_parent(obj) == list)
|
||||
return lv_obj_get_index(obj);
|
||||
}
|
||||
ESP_LOGW(TAG, "lvgl.list: entry is not inside the list it was added to");
|
||||
return -1;
|
||||
}
|
||||
|
||||
lv_obj_t *lv_list_get_row_for_remove(lv_obj_t *list, int index) {
|
||||
lv_obj_t *child = index < 0 ? nullptr : lv_obj_get_child(list, index);
|
||||
if (child == nullptr) {
|
||||
ESP_LOGW(TAG, "lvgl.list.remove: index %d is out of range, ignoring", index);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
#endif // USE_LVGL_LIST
|
||||
} // namespace esphome::lvgl
|
||||
|
||||
lv_result_t lv_mem_test_core() { return LV_RESULT_OK; }
|
||||
|
||||
@@ -116,18 +116,6 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images
|
||||
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value);
|
||||
#endif
|
||||
|
||||
#ifdef USE_LVGL_LIST
|
||||
// Returns the index, within `list`, of the entry that contains `child`: `child` itself if it's a
|
||||
// direct child of `list`, or the ancestor of `child` that is, when `child` is nested inside a
|
||||
// widget hierarchy added via `lvgl.list.add`. Returns -1 if `child` isn't inside `list` at all.
|
||||
int lv_list_get_row_index(lv_obj_t *list, lv_obj_t *child);
|
||||
|
||||
// Returns the entry at `index` within `list`, or nullptr (logging why) if `index` is out of
|
||||
// range -- shared by every `lvgl.list.remove` call site, since a templatable index can go out of
|
||||
// range at runtime in ways config validation can't catch (e.g. driven by a sensor value).
|
||||
lv_obj_t *lv_list_get_row_for_remove(lv_obj_t *list, int index);
|
||||
#endif
|
||||
|
||||
#ifdef USE_LVGL_GRADIENT
|
||||
/**
|
||||
*
|
||||
@@ -147,12 +135,6 @@ class LvCompound {
|
||||
lv_obj_t *obj{};
|
||||
};
|
||||
|
||||
// Frees a heap-allocated LvCompound wrapper on LV_EVENT_DELETE, since lv_obj_del() only knows how to destroy LVGL's own
|
||||
// object tree, not a separate C++ object paired with one of its nodes.
|
||||
template<typename T> void delete_lv_compound_on_delete(lv_event_t *e) {
|
||||
delete static_cast<T *>(lv_event_get_user_data(e));
|
||||
}
|
||||
|
||||
class LvglComponent;
|
||||
|
||||
class LvPageType : public Parented<LvglComponent> {
|
||||
@@ -259,11 +241,10 @@ class LvglComponent final : public PollingComponent {
|
||||
static void esphome_lvgl_init();
|
||||
|
||||
// Convenience overloads for adding a callback for one or more events
|
||||
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event, void *user_data = nullptr);
|
||||
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event);
|
||||
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
|
||||
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2,
|
||||
void *user_data = nullptr);
|
||||
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2,
|
||||
lv_event_code_t event3, void *user_data = nullptr);
|
||||
lv_event_code_t event3);
|
||||
|
||||
// change the state of a widget and fire an event if changed (only needed for CHECKED)
|
||||
|
||||
|
||||
@@ -726,26 +726,6 @@ ALL_STYLES = {
|
||||
}
|
||||
|
||||
|
||||
def apply_style_driven_defines(props: set[str]) -> None:
|
||||
"""Given a set of style-property names in use, registers everything their use
|
||||
drives: add_lv_use(image) if any of them is image-typed (per BASE_PROPS), and
|
||||
the LV_COLOR_SCREEN_TRANSP / LV_DRAW_SW_SUPPORT_A8 defines. Shared between
|
||||
__init__.py (driven by df.get_styles_used(), for statically-declared widgets)
|
||||
and lv_list.py's _register_dynamic_widget_style_uses (driven by scanning a
|
||||
dynamically-added widget's own config), so a future style-driven define added
|
||||
to one can't be missed in the other.
|
||||
"""
|
||||
# Local import: avoids a module-load-time cycle (widgets.img -> ... -> schemas).
|
||||
from .widgets.img import CONF_IMAGE
|
||||
|
||||
if any(BASE_PROPS.get(prop) is lvalid.lv_image for prop in props):
|
||||
df.add_lv_use(CONF_IMAGE)
|
||||
if df.TRANSFORM_STYLE_PROPS & props:
|
||||
df.add_define("LV_COLOR_SCREEN_TRANSP", "1")
|
||||
if df.DROP_SHADOW_STYLE_PROPS & props:
|
||||
df.add_define("LV_DRAW_SW_SUPPORT_A8", "1")
|
||||
|
||||
|
||||
def strip_defaults(schema: cv.Schema):
|
||||
"""
|
||||
Take a schema and remove any default values, also convert Required to Optional.
|
||||
|
||||
@@ -59,10 +59,7 @@ async def generate_triggers():
|
||||
all_triggers = (
|
||||
LV_EVENT_TRIGGERS + LV_DISPLAY_EVENT_TRIGGERS + LV_SCREEN_EVENT_TRIGGERS
|
||||
)
|
||||
# Snapshot: building a trigger below can recurse into widget creation (e.g. a
|
||||
# buttonmatrix's or tabview's to_code registers its own child widgets), which
|
||||
# would otherwise mutate this dict mid-iteration.
|
||||
for w in list(get_widget_map().values()):
|
||||
for w in get_widget_map().values():
|
||||
config = w.config
|
||||
if isinstance(w.type, LvScrActType):
|
||||
w = get_screen_active(w.var)
|
||||
@@ -144,21 +141,7 @@ def _get_event_literal(trigger: str | MockObj) -> MockObj:
|
||||
return literal("LV_EVENT_" + TRIGGER_MAP[trigger.upper()])
|
||||
|
||||
|
||||
async def add_trigger(
|
||||
conf, w, *events: str | MockObj, is_selected=None, attach_obj=None, user_data=None
|
||||
):
|
||||
"""
|
||||
:param attach_obj: The object to actually register the callback on, if different
|
||||
from `w.obj` - used when `w.obj` isn't valid at the point the callback gets
|
||||
registered (e.g. a local variable that's only in scope inside the very
|
||||
block this is called from, not from within the callback body itself; see
|
||||
widgets/lv_list.py's dynamic widget creation). Defaults to `w.obj`.
|
||||
:param user_data: Opaque pointer passed through to the registered event callback,
|
||||
retrievable inside it via `lv_event_get_user_data(event)` - used to recover a
|
||||
compound widget's C++ wrapper, which a captureless callback has no other way
|
||||
to reach when it isn't a global variable (see widgets/lv_list.py). Defaults to
|
||||
`nullptr`.
|
||||
"""
|
||||
async def add_trigger(conf, w, *events: str | MockObj, is_selected=None):
|
||||
is_selected = is_selected or w.is_selected()
|
||||
tid = conf[CONF_TRIGGER_ID]
|
||||
trigger = cg.new_Pvariable(tid)
|
||||
@@ -175,14 +158,12 @@ async def add_trigger(
|
||||
lv_add(trigger.trigger(*value, literal("event")))
|
||||
callback = await context.get_lambda()
|
||||
event_literals = [_get_event_literal(event) for event in events]
|
||||
attach_obj = w.obj if attach_obj is None else attach_obj
|
||||
user_data = nullptr if user_data is None else user_data
|
||||
if str(events[0]) in DISPLAY_TRIGGERS:
|
||||
assert len(events) == 1
|
||||
lv.display_add_event_cb(
|
||||
lv_expr.obj_get_display(attach_obj), callback, event_literals[0], user_data
|
||||
lv_expr.obj_get_display(w.obj), callback, event_literals[0], nullptr
|
||||
)
|
||||
else:
|
||||
lv_add(
|
||||
lvgl_static.add_event_cb(attach_obj, callback, *event_literals, user_data)
|
||||
lvgl_static.add_event_cb(w.obj, await context.get_lambda(), *event_literals)
|
||||
)
|
||||
|
||||
@@ -190,7 +190,18 @@ class WidgetType:
|
||||
await self.on_create(var, config)
|
||||
|
||||
w = Widget.create(wid, var, self, config)
|
||||
apply_theme_styles(w)
|
||||
if theme := get_theme_widget_map().get(self.name):
|
||||
for part, states in theme.items():
|
||||
part = "LV_PART_" + part.upper()
|
||||
for state, style in states.items():
|
||||
state = "LV_STATE_" + state.upper()
|
||||
if state == "LV_STATE_DEFAULT":
|
||||
lv_state = literal(part)
|
||||
elif part == "LV_PART_MAIN":
|
||||
lv_state = literal(state)
|
||||
else:
|
||||
lv_state = join_enums((state, part))
|
||||
w.add_style(style, lv_state)
|
||||
await set_obj_properties(w, config)
|
||||
await add_widgets(w, config)
|
||||
await self.to_code(w, config)
|
||||
@@ -219,7 +230,7 @@ class WidgetType:
|
||||
:param config: Its configuration
|
||||
"""
|
||||
|
||||
def get_uses(self) -> tuple:
|
||||
def get_uses(self):
|
||||
"""
|
||||
Get a list of other widgets used by this one
|
||||
:return:
|
||||
@@ -256,21 +267,6 @@ class WidgetType:
|
||||
"""
|
||||
|
||||
|
||||
def apply_theme_styles(w: "Widget") -> None:
|
||||
"""Apply the current theme's styles for this widget's type"""
|
||||
for part, states in get_theme_widget_map().get(w.type.name, {}).items():
|
||||
part = "LV_PART_" + part.upper()
|
||||
for state, style in states.items():
|
||||
state = "LV_STATE_" + state.upper()
|
||||
if state == "LV_STATE_DEFAULT":
|
||||
lv_state = literal(part)
|
||||
elif part == "LV_PART_MAIN":
|
||||
lv_state = literal(state)
|
||||
else:
|
||||
lv_state = join_enums((state, part))
|
||||
w.add_style(style, lv_state)
|
||||
|
||||
|
||||
class Widget:
|
||||
"""
|
||||
Represents a Widget.
|
||||
|
||||
@@ -1,553 +0,0 @@
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUTTON,
|
||||
CONF_ID,
|
||||
CONF_INDEX,
|
||||
CONF_ON_BOOT,
|
||||
CONF_ON_UPDATE,
|
||||
CONF_ON_VALUE,
|
||||
CONF_TEXT,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.coroutine import FakeAwaitable
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
CONF_ALIGN_TO,
|
||||
CONF_MAIN,
|
||||
CONF_PAD_ROW,
|
||||
CONF_SCROLLBAR,
|
||||
CONF_WIDGETS,
|
||||
LV_EVENT_TRIGGERS,
|
||||
SWIPE_TRIGGERS,
|
||||
TYPE_FLEX,
|
||||
add_lv_use,
|
||||
literal,
|
||||
)
|
||||
from ..lv_validation import lv_int, lv_text, padding
|
||||
from ..lvcode import (
|
||||
UPDATE_EVENT,
|
||||
LocalVariable,
|
||||
LvConditional,
|
||||
LvCountdown,
|
||||
lv,
|
||||
lv_add,
|
||||
lv_expr,
|
||||
lv_obj,
|
||||
)
|
||||
from ..schemas import (
|
||||
ALL_STYLES,
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
apply_style_driven_defines,
|
||||
container_schema_value,
|
||||
remap_property,
|
||||
)
|
||||
from ..trigger import add_trigger
|
||||
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t
|
||||
from . import (
|
||||
Widget,
|
||||
WidgetType,
|
||||
apply_theme_styles,
|
||||
collect_parts,
|
||||
get_widgets,
|
||||
set_obj_properties,
|
||||
)
|
||||
from .buttonmatrix import CONF_BUTTONMATRIX
|
||||
from .canvas import CONF_CANVAS
|
||||
from .label import CONF_LABEL
|
||||
from .meter import CONF_METER
|
||||
from .tabview import CONF_TABVIEW
|
||||
from .tileview import CONF_TILEVIEW
|
||||
|
||||
CONF_LIST = "list"
|
||||
CONF_WIDGET = "widget"
|
||||
CONF_ON_ADD = "on_add"
|
||||
CONF_ON_REMOVE = "on_remove"
|
||||
|
||||
DOMAIN = "lvgl_list"
|
||||
|
||||
lv_list_t = LvType("lv_list_t")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListTriggers:
|
||||
on_add: list = field(default_factory=list)
|
||||
on_remove: list = field(default_factory=list)
|
||||
|
||||
|
||||
def _get_list_triggers(list_id) -> ListTriggers:
|
||||
"""
|
||||
Trigger Pvariables built for a given list's `on_add`/`on_remove` config, indexed by the
|
||||
list's own ID.
|
||||
"""
|
||||
triggers_by_list = CORE.data.setdefault(DOMAIN, {})
|
||||
return triggers_by_list.setdefault(list_id, ListTriggers())
|
||||
|
||||
|
||||
def _get_pending_list_triggers(list_id) -> ListTriggers:
|
||||
"""
|
||||
Same shape as _get_list_triggers(), but holding raw on_add/on_remove automation
|
||||
configs, not yet built.
|
||||
"""
|
||||
pending_by_list = CORE.data.setdefault(DOMAIN + "_pending", {})
|
||||
return pending_by_list.setdefault(list_id, ListTriggers())
|
||||
|
||||
|
||||
def _list_triggers_completed_flag() -> list[bool]:
|
||||
return CORE.data.setdefault(DOMAIN + "_completed", [False])
|
||||
|
||||
|
||||
def _list_triggers_completed_generator() -> Generator[None, None, None]:
|
||||
while True:
|
||||
if _list_triggers_completed_flag()[0]:
|
||||
return
|
||||
yield
|
||||
|
||||
|
||||
async def _wait_list_triggers_completed() -> None:
|
||||
"""Waits until finish_list_triggers() has built every list's on_add/on_remove automations."""
|
||||
if _list_triggers_completed_flag()[0]:
|
||||
return
|
||||
await FakeAwaitable(_list_triggers_completed_generator())
|
||||
|
||||
|
||||
async def finish_list_triggers() -> None:
|
||||
"""
|
||||
Builds every list's on_add/on_remove automations, collected by ListType.to_code()
|
||||
instead of being built there directly. Must run after set_widgets_completed(True).
|
||||
"""
|
||||
for list_id, pending in CORE.data.get(DOMAIN + "_pending", {}).items():
|
||||
triggers = _get_list_triggers(list_id)
|
||||
for conf in pending.on_add:
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
await automation.build_automation(trigger, [(cg.int_, "list_index")], conf)
|
||||
triggers.on_add.append(trigger)
|
||||
for conf in pending.on_remove:
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
await automation.build_automation(trigger, [(cg.int_, "list_index")], conf)
|
||||
triggers.on_remove.append(trigger)
|
||||
_list_triggers_completed_flag()[0] = True
|
||||
|
||||
|
||||
def _fire_index_triggers(triggers: list, index) -> None:
|
||||
for trigger in triggers:
|
||||
lv_add(trigger.trigger(index))
|
||||
|
||||
|
||||
async def _fire_on_add(list_id, list_obj, entry_obj) -> None:
|
||||
await _wait_list_triggers_completed()
|
||||
triggers = _get_list_triggers(list_id).on_add
|
||||
if not triggers:
|
||||
return
|
||||
index = cg.RawExpression(f"lvgl::lv_list_get_row_index({list_obj}, {entry_obj})")
|
||||
_fire_index_triggers(triggers, index)
|
||||
|
||||
|
||||
async def _fire_on_remove(list_id, index) -> None:
|
||||
await _wait_list_triggers_completed()
|
||||
_fire_index_triggers(_get_list_triggers(list_id).on_remove, index)
|
||||
|
||||
|
||||
LIST_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_PAD_ROW): padding,
|
||||
}
|
||||
)
|
||||
|
||||
LIST_CREATE_SCHEMA = LIST_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_ADD): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
automation.Trigger.template(cg.int_)
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_REMOVE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
automation.Trigger.template(cg.int_)
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ListType(WidgetType):
|
||||
"""A plain wrapper around LVGL's native `lv_list`"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
CONF_LIST,
|
||||
lv_list_t,
|
||||
(CONF_MAIN, CONF_SCROLLBAR),
|
||||
LIST_CREATE_SCHEMA,
|
||||
modify_schema=LIST_SCHEMA,
|
||||
)
|
||||
|
||||
def get_uses(self):
|
||||
return TYPE_FLEX, CONF_LABEL, CONF_BUTTON
|
||||
|
||||
async def to_code(self, w: Widget, config: dict):
|
||||
on_add = config.get(CONF_ON_ADD, ())
|
||||
on_remove = config.get(CONF_ON_REMOVE, ())
|
||||
if not on_add and not on_remove:
|
||||
return
|
||||
pending = _get_pending_list_triggers(w.config[CONF_ID])
|
||||
pending.on_add.extend(on_add)
|
||||
pending.on_remove.extend(on_remove)
|
||||
|
||||
|
||||
list_spec = ListType()
|
||||
|
||||
LIST_ID_SCHEMA = cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_list_t)})
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.list.add_text",
|
||||
ObjUpdateAction,
|
||||
LIST_ID_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_TEXT): lv_text,
|
||||
cv.Optional(CONF_INDEX): cv.templatable(cv.int_),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def list_add_text_to_code(config, action_id, template_arg, args):
|
||||
widgets = await get_widgets(config)
|
||||
|
||||
async def do_add_text(w: Widget):
|
||||
text = await lv_text.process(config[CONF_TEXT])
|
||||
with LocalVariable(
|
||||
"list_entry", lv_obj_t, lv_expr.list_add_text(w.obj, text)
|
||||
) as entry:
|
||||
if (idx := config.get(CONF_INDEX)) is not None:
|
||||
lv.obj_move_to_index(entry, await lv_int.process(idx))
|
||||
await _fire_on_add(config[CONF_ID], w.obj, entry)
|
||||
|
||||
return await action_to_code(
|
||||
widgets, do_add_text, action_id, template_arg, args, config
|
||||
)
|
||||
|
||||
|
||||
_DYNAMIC_WIDGET_UNSUPPORTED = (
|
||||
CONF_BUTTONMATRIX,
|
||||
CONF_TABVIEW,
|
||||
CONF_TILEVIEW,
|
||||
CONF_METER,
|
||||
CONF_CANVAS,
|
||||
)
|
||||
|
||||
|
||||
def _check_dynamic_widget_supported(w_type_name: str, w_conf: dict) -> None:
|
||||
# Each of these allocates a Pvariable, or registers children into the global widget
|
||||
# map, once at boot - rebuilding them on every lvgl.list.add call would break that.
|
||||
if w_type_name in _DYNAMIC_WIDGET_UNSUPPORTED:
|
||||
raise cv.Invalid(
|
||||
f"'{w_type_name}' cannot be used with lvgl.list.add - it manages its own "
|
||||
"child widgets in a way that isn't compatible with widgets created at runtime"
|
||||
)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
[(child_type, child_conf)] = child.items()
|
||||
_check_dynamic_widget_supported(child_type, child_conf)
|
||||
|
||||
|
||||
_UNSUPPORTED_DYNAMIC_KEYS = SWIPE_TRIGGERS + (CONF_ON_BOOT, CONF_ALIGN_TO)
|
||||
|
||||
|
||||
def _check_no_unsupported_triggers(w_type_name: str, w_conf: dict) -> None:
|
||||
# These triggers currently aren't supporte for dynamic widgets
|
||||
for key in _UNSUPPORTED_DYNAMIC_KEYS:
|
||||
if key in w_conf:
|
||||
raise cv.Invalid(
|
||||
f"'{key}' is not supported on a widget added via lvgl.list.add - it "
|
||||
"would validate but generate nothing, since it's only wired for "
|
||||
"widgets that exist at boot",
|
||||
path=[w_type_name, key],
|
||||
)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
[(child_type, child_conf)] = child.items()
|
||||
_check_no_unsupported_triggers(child_type, child_conf)
|
||||
|
||||
|
||||
def _check_no_explicit_widget_id(raw_value: dict) -> None:
|
||||
for w_type_name, w_conf in raw_value.items():
|
||||
if not isinstance(w_conf, dict):
|
||||
continue
|
||||
if CONF_ID in w_conf:
|
||||
raise cv.Invalid(
|
||||
"'id' is not allowed on a widget added via lvgl.list.add - it is "
|
||||
"rebuilt fresh on every call and never registered anywhere it "
|
||||
"could be looked up by",
|
||||
path=[w_type_name, CONF_ID],
|
||||
)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
if isinstance(child, dict):
|
||||
_check_no_explicit_widget_id(child)
|
||||
|
||||
|
||||
@schema_extractor("schema")
|
||||
def list_add_schema(value: Any) -> Any:
|
||||
# A plain cv.Schema can't express "id, an optional index, plus exactly one arbitrary
|
||||
# widget-type key", since the set of widget types isn't fixed until validation time.
|
||||
if value is SCHEMA_EXTRACT:
|
||||
return LIST_ID_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_INDEX): cv.templatable(cv.int_),
|
||||
**{
|
||||
cv.Optional(name): container_schema_value(widget_type)
|
||||
for name, widget_type in WIDGET_TYPES.items()
|
||||
},
|
||||
}
|
||||
)
|
||||
if not isinstance(value, dict):
|
||||
raise cv.Invalid("Expected a mapping")
|
||||
value = value.copy()
|
||||
if CONF_ID not in value:
|
||||
raise cv.Invalid(f"required key '{CONF_ID}' not provided")
|
||||
with cv.prepend_path([CONF_ID]):
|
||||
list_id = cv.use_id(lv_list_t)(value.pop(CONF_ID))
|
||||
result = {CONF_ID: list_id}
|
||||
if CONF_INDEX in value:
|
||||
with cv.prepend_path([CONF_INDEX]):
|
||||
result[CONF_INDEX] = cv.templatable(cv.int_)(value.pop(CONF_INDEX))
|
||||
if len(value) != 1:
|
||||
raise cv.Invalid(
|
||||
"lvgl.list.add takes exactly one widget definition, e.g. 'label:' or 'button:', alongside 'id' and optional 'index'"
|
||||
)
|
||||
_check_no_explicit_widget_id(value)
|
||||
result[CONF_WIDGET] = any_widget_schema()(value)
|
||||
[(w_type_name, w_conf)] = result[CONF_WIDGET][0].items()
|
||||
_check_dynamic_widget_supported(w_type_name, w_conf)
|
||||
_check_no_unsupported_triggers(w_type_name, w_conf)
|
||||
return result
|
||||
|
||||
|
||||
def _register_lv_uses(w_type_name: str, w_conf: dict) -> None:
|
||||
# Must run before this coroutine's first await.
|
||||
widget_type = WIDGET_TYPES[w_type_name]
|
||||
add_lv_use(w_type_name)
|
||||
add_lv_use(*widget_type.get_uses())
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
[(child_type, child_conf)] = child.items()
|
||||
_register_lv_uses(child_type, child_conf)
|
||||
|
||||
|
||||
def _register_dynamic_widget_style_uses(w_conf: dict) -> None:
|
||||
props = {
|
||||
remap_property(prop)
|
||||
for part_states in collect_parts(w_conf).values()
|
||||
for state_props in part_states.values()
|
||||
for prop in state_props
|
||||
if prop in ALL_STYLES
|
||||
}
|
||||
apply_style_driven_defines(props)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
[(_, child_conf)] = child.items()
|
||||
_register_dynamic_widget_style_uses(child_conf)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.list.add",
|
||||
ObjUpdateAction,
|
||||
list_add_schema,
|
||||
synchronous=True,
|
||||
)
|
||||
async def list_add_to_code(config, action_id, template_arg, args):
|
||||
[(w_type_name, w_conf)] = config[CONF_WIDGET][0].items()
|
||||
_register_lv_uses(w_type_name, w_conf)
|
||||
_register_dynamic_widget_style_uses(w_conf)
|
||||
widgets = await get_widgets(config)
|
||||
|
||||
async def do_add(w: Widget):
|
||||
index = None
|
||||
if (idx := config.get(CONF_INDEX)) is not None:
|
||||
index = await lv_int.process(idx)
|
||||
await _build_dynamic_widget(
|
||||
w_type_name,
|
||||
w_conf,
|
||||
w.obj,
|
||||
config[CONF_ID],
|
||||
w.obj,
|
||||
top_level=True,
|
||||
index=index,
|
||||
)
|
||||
|
||||
return await action_to_code(widgets, do_add, action_id, template_arg, args, config)
|
||||
|
||||
|
||||
async def _build_dynamic_widget(
|
||||
w_type_name: str,
|
||||
w_conf: dict,
|
||||
parent,
|
||||
list_id,
|
||||
list_obj,
|
||||
top_level: bool = False,
|
||||
index=None,
|
||||
depth: int = 0,
|
||||
) -> None:
|
||||
# Builds one widget (recursively, with children and triggers) as a LocalVariable
|
||||
# instead of a global Pvariable. Compound
|
||||
# widgets are heap-allocated and freed via LV_EVENT_DELETE.
|
||||
# `depth` suffixes the local variable's name below the row's top level.
|
||||
widget_type = WIDGET_TYPES[w_type_name]
|
||||
var_name = f"dyn_{w_type_name}" if depth == 0 else f"dyn_{w_type_name}_{depth}"
|
||||
add_lv_use(w_type_name)
|
||||
add_lv_use(*widget_type.get_uses())
|
||||
|
||||
async def finish_and_fire(w: Widget) -> None:
|
||||
# Shared tail for both branches below - must run while var's LocalVariable
|
||||
# block (opened by whichever branch calls this) is still open
|
||||
await _finish_dynamic_widget(w, w_conf, list_id, list_obj, depth)
|
||||
if top_level:
|
||||
if index is not None:
|
||||
lv.obj_move_to_index(w.obj, index)
|
||||
await _fire_on_add(list_id, list_obj, w.obj)
|
||||
|
||||
if widget_type.is_compound():
|
||||
with LocalVariable(
|
||||
var_name, widget_type.w_type, widget_type.w_type.new()
|
||||
) as var:
|
||||
creator = await widget_type.obj_creator(parent, w_conf)
|
||||
lv_add(var.set_obj(creator))
|
||||
w = Widget(var, widget_type, w_conf)
|
||||
lv_obj.add_event_cb(
|
||||
w.obj,
|
||||
literal(f"lvgl::delete_lv_compound_on_delete<{widget_type.w_type}>"),
|
||||
literal("LV_EVENT_DELETE"),
|
||||
var,
|
||||
)
|
||||
await finish_and_fire(w)
|
||||
else:
|
||||
creator = await widget_type.obj_creator(parent, w_conf)
|
||||
with LocalVariable(var_name, lv_obj_t, creator) as var:
|
||||
w = Widget(var, widget_type, w_conf)
|
||||
await finish_and_fire(w)
|
||||
|
||||
|
||||
async def _finish_dynamic_widget(
|
||||
w: Widget, w_conf: dict, list_id, list_obj, depth: int = 0
|
||||
) -> None:
|
||||
await w.type.on_create(w.obj, w_conf)
|
||||
apply_theme_styles(w)
|
||||
await set_obj_properties(w, w_conf)
|
||||
await w.type.to_code(w, w_conf)
|
||||
await _wire_dynamic_triggers(w, w_conf)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
[(child_type, child_conf)] = child.items()
|
||||
await _build_dynamic_widget(
|
||||
child_type, child_conf, w.obj, list_id, list_obj, depth=depth + 1
|
||||
)
|
||||
|
||||
|
||||
async def _wire_dynamic_triggers(w: Widget, config: dict) -> None:
|
||||
# Mirrors generate_triggers(), but runs immediately
|
||||
if w.type.is_compound():
|
||||
event_var = MockObj(
|
||||
f"static_cast<{w.type.w_type} *>(lv_event_get_user_data(event))", "->"
|
||||
)
|
||||
user_data = w.var
|
||||
else:
|
||||
event_var = literal("static_cast<lv_obj_t *>(lv_event_get_target(event))")
|
||||
user_data = None
|
||||
event_target = Widget(event_var, w.type, config)
|
||||
for event, conf in {
|
||||
event: conf for event, conf in config.items() if event in LV_EVENT_TRIGGERS
|
||||
}.items():
|
||||
w.add_flag("LV_OBJ_FLAG_CLICKABLE")
|
||||
await add_trigger(
|
||||
conf[0], event_target, event, attach_obj=w.obj, user_data=user_data
|
||||
)
|
||||
for conf in config.get(CONF_ON_VALUE, ()):
|
||||
await add_trigger(
|
||||
conf,
|
||||
event_target,
|
||||
LV_EVENT.VALUE_CHANGED,
|
||||
UPDATE_EVENT,
|
||||
attach_obj=w.obj,
|
||||
user_data=user_data,
|
||||
)
|
||||
for conf in config.get(CONF_ON_UPDATE, ()):
|
||||
await add_trigger(
|
||||
conf, event_target, UPDATE_EVENT, attach_obj=w.obj, user_data=user_data
|
||||
)
|
||||
|
||||
|
||||
LIST_REMOVE_SCHEMA = LIST_ID_SCHEMA.extend(
|
||||
{
|
||||
# positive_int, not int_: a negative index would silently delete the *last*
|
||||
# row (lv_obj_get_child() counts back from the end) while reporting that
|
||||
# same bogus value to on_remove's list_index.
|
||||
cv.Required(CONF_INDEX): cv.templatable(cv.positive_int),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.list.remove",
|
||||
ObjUpdateAction,
|
||||
LIST_REMOVE_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def list_remove_to_code(config, action_id, template_arg, args):
|
||||
widgets = await get_widgets(config)
|
||||
|
||||
async def do_remove(w: Widget):
|
||||
index = await lv_int.process(config[CONF_INDEX])
|
||||
# Materialised into a local since index is needed at two call sites below, and
|
||||
# a lambda's body gets re-emitted (and re-run) at every point it's used.
|
||||
with (
|
||||
LocalVariable("list_index", cg.int_, index, modifier="") as idx,
|
||||
# Out-of-range lookup/log lives in a shared C++ helper, not inline here:
|
||||
# a config can have many lvgl.list.remove call sites.
|
||||
LocalVariable(
|
||||
"list_child",
|
||||
lv_obj_t,
|
||||
cg.RawExpression(f"lvgl::lv_list_get_row_for_remove({w.obj}, {idx})"),
|
||||
) as child,
|
||||
LvConditional(child),
|
||||
):
|
||||
await _fire_on_remove(config[CONF_ID], idx)
|
||||
# Recursively destroys the whole subtree
|
||||
lv.obj_del(child)
|
||||
|
||||
return await action_to_code(
|
||||
widgets, do_remove, action_id, template_arg, args, config
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.list.clear",
|
||||
ObjUpdateAction,
|
||||
LIST_ID_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def list_clear_to_code(config, action_id, template_arg, args):
|
||||
widgets = await get_widgets(config)
|
||||
|
||||
async def do_clear(w: Widget):
|
||||
await _wait_list_triggers_completed()
|
||||
triggers = _get_list_triggers(config[CONF_ID]).on_remove
|
||||
if triggers:
|
||||
# Fire on_remove for every entry, newest to oldest, before wiping them all out,
|
||||
# so on_remove's semantics ("an entry left the list") hold
|
||||
with LvCountdown("list_index", lv_expr.obj_get_child_count(w.obj)) as index:
|
||||
_fire_index_triggers(triggers, index)
|
||||
# lv_obj_clean recursively destroys every child's whole subtree
|
||||
lv.obj_clean(w.obj)
|
||||
|
||||
return await action_to_code(
|
||||
widgets, do_clear, action_id, template_arg, args, config
|
||||
)
|
||||
@@ -345,7 +345,7 @@ int MipiRgb::get_height() {
|
||||
}
|
||||
}
|
||||
|
||||
[[maybe_unused]] static const char *get_pin_name(GPIOPin *pin, std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
|
||||
static const char *get_pin_name(GPIOPin *pin, std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
|
||||
if (pin == nullptr)
|
||||
return "None";
|
||||
pin->dump_summary(buffer.data(), buffer.size());
|
||||
|
||||
@@ -49,13 +49,6 @@ void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); }
|
||||
|
||||
bool MitsubishiCN105::update() {
|
||||
switch (this->state_) {
|
||||
case State::DEFERRED_STATUS_REQUEST:
|
||||
// Defer the next request to a later loop iteration; some units might not respond if a request is sent
|
||||
// immediately after a response. See https://github.com/esphome/esphome/issues/18099. No minimum RX-to-TX delay
|
||||
// is enforced.
|
||||
this->set_state_(State::UPDATING_STATUS);
|
||||
return false;
|
||||
|
||||
case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE:
|
||||
if (this->pending_updates_.any()) {
|
||||
this->status_update_wait_credit_ms_ =
|
||||
@@ -108,14 +101,12 @@ bool MitsubishiCN105::should_transition(State from, State to) {
|
||||
return from == State::CONNECTING;
|
||||
|
||||
case State::UPDATING_STATUS:
|
||||
return from == State::DEFERRED_STATUS_REQUEST || from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE;
|
||||
return from == State::CONNECTED || from == State::STATUS_UPDATED ||
|
||||
from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE;
|
||||
|
||||
case State::STATUS_UPDATED:
|
||||
return from == State::UPDATING_STATUS;
|
||||
|
||||
case State::DEFERRED_STATUS_REQUEST:
|
||||
return from == State::CONNECTED || from == State::STATUS_UPDATED;
|
||||
|
||||
case State::SCHEDULE_NEXT_STATUS_UPDATE:
|
||||
return from == State::STATUS_UPDATED || from == State::SETTINGS_APPLIED;
|
||||
|
||||
@@ -123,7 +114,7 @@ bool MitsubishiCN105::should_transition(State from, State to) {
|
||||
return from == State::SCHEDULE_NEXT_STATUS_UPDATE;
|
||||
|
||||
case State::APPLYING_SETTINGS:
|
||||
return from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE;
|
||||
return from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE || from == State::STATUS_UPDATED;
|
||||
|
||||
case State::SETTINGS_APPLIED:
|
||||
return from == State::APPLYING_SETTINGS;
|
||||
@@ -131,10 +122,9 @@ bool MitsubishiCN105::should_transition(State from, State to) {
|
||||
case State::READ_TIMEOUT:
|
||||
return from == State::UPDATING_STATUS || from == State::APPLYING_SETTINGS || from == State::CONNECTING;
|
||||
|
||||
case State::NOT_CONNECTED:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::did_transition_(State to) {
|
||||
@@ -145,7 +135,7 @@ void MitsubishiCN105::did_transition_(State to) {
|
||||
|
||||
case State::CONNECTED:
|
||||
this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
|
||||
this->set_state_(State::DEFERRED_STATUS_REQUEST);
|
||||
this->set_state_(State::UPDATING_STATUS);
|
||||
break;
|
||||
|
||||
case State::UPDATING_STATUS:
|
||||
@@ -153,14 +143,11 @@ void MitsubishiCN105::did_transition_(State to) {
|
||||
break;
|
||||
|
||||
case State::STATUS_UPDATED: {
|
||||
// When present, pending settings are applied from WAITING_FOR_SCHEDULED_STATUS_UPDATE during the next update(),
|
||||
// deferring transmission to a later loop iteration; some units might not respond if a request is sent
|
||||
// immediately after a response, causing the request to time out.
|
||||
const bool should_apply_pending_settings = this->pending_updates_.any() && this->is_status_initialized();
|
||||
if (!should_apply_pending_settings && this->current_status_msg_type_ == STATUS_MSG_SETTINGS &&
|
||||
this->should_request_telemetry_()) {
|
||||
if (this->pending_updates_.any() && this->is_status_initialized()) {
|
||||
this->set_state_(State::APPLYING_SETTINGS);
|
||||
} else if (this->current_status_msg_type_ == STATUS_MSG_SETTINGS && this->should_request_telemetry_()) {
|
||||
this->current_status_msg_type_ = STATUS_MSG_TELEMETRY;
|
||||
this->set_state_(State::DEFERRED_STATUS_REQUEST);
|
||||
this->set_state_(State::UPDATING_STATUS);
|
||||
} else {
|
||||
this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE);
|
||||
}
|
||||
@@ -188,9 +175,7 @@ void MitsubishiCN105::did_transition_(State to) {
|
||||
this->set_state_(State::CONNECTING);
|
||||
break;
|
||||
|
||||
case State::NOT_CONNECTED:
|
||||
case State::DEFERRED_STATUS_REQUEST:
|
||||
case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -374,8 +359,6 @@ const LogString *MitsubishiCN105::state_to_string(State state) {
|
||||
return LOG_STR("UpdatingStatus");
|
||||
case State::STATUS_UPDATED:
|
||||
return LOG_STR("StatusUpdated");
|
||||
case State::DEFERRED_STATUS_REQUEST:
|
||||
return LOG_STR("DeferredStatusRequest");
|
||||
case State::SCHEDULE_NEXT_STATUS_UPDATE:
|
||||
return LOG_STR("ScheduleNextStatusUpdate");
|
||||
case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE:
|
||||
|
||||
@@ -101,7 +101,6 @@ class MitsubishiCN105 {
|
||||
CONNECTED,
|
||||
UPDATING_STATUS,
|
||||
STATUS_UPDATED,
|
||||
DEFERRED_STATUS_REQUEST,
|
||||
SCHEDULE_NEXT_STATUS_UPDATE,
|
||||
WAITING_FOR_SCHEDULED_STATUS_UPDATE,
|
||||
APPLYING_SETTINGS,
|
||||
|
||||
@@ -45,7 +45,6 @@ ModbusClient = modbus_ns.class_("ModbusClientHub", Modbus)
|
||||
ModbusDevice = modbus_ns.class_("ModbusDevice")
|
||||
ModbusClientDevice = modbus_ns.class_("ModbusClientDevice")
|
||||
ModbusServerDevice = modbus_ns.class_("ModbusServerDevice")
|
||||
CommandOptions = modbus_ns.struct("CommandOptions")
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_ROLE = "role"
|
||||
@@ -82,19 +81,6 @@ def _command_options(direction: str) -> list[_CommandOption]:
|
||||
raise ValueError(f"unknown command-options direction {direction!r}") from None
|
||||
|
||||
|
||||
# The write (mutating) function codes, matching modbus::helpers::is_function_code_write(). 0x17
|
||||
# (read/write multiple) is included: it mutates, so the hub treats it as a write despite its read half.
|
||||
_WRITE_FUNCTION_CODES = frozenset({0x05, 0x06, 0x0F, 0x10, 0x16, 0x17})
|
||||
|
||||
|
||||
def is_function_code_write(function_code: int) -> bool:
|
||||
"""True if the Modbus function code writes (mutates). The exception bit (0x80) is masked off first,
|
||||
so an exception-flagged code still classifies by its base code - stricter than the runtime hub,
|
||||
whose classify() treats an exception-flagged code as a read. Keep in sync with
|
||||
modbus::helpers::is_function_code_write()."""
|
||||
return function_code & 0x7F in _WRITE_FUNCTION_CODES
|
||||
|
||||
|
||||
def command_options_schema(
|
||||
*, direction: Literal["read", "write"], templatable: bool = False
|
||||
) -> dict[cv.Optional, Any]:
|
||||
@@ -112,25 +98,6 @@ def command_options_schema(
|
||||
}
|
||||
|
||||
|
||||
def command_options_expression(
|
||||
config: ConfigType, *, direction: Literal["read", "write"]
|
||||
) -> cg.StructInitializer:
|
||||
"""Build the modbus::CommandOptions initializer for a config validated with
|
||||
command_options_schema() of the same direction. For static (non-templatable) options only;
|
||||
actions with lambda values use register_templatable_command_options() instead.
|
||||
"""
|
||||
return cg.StructInitializer(
|
||||
CommandOptions,
|
||||
*(
|
||||
# Construct the value as its declared cpp_type, so a future non-bool option (enum,
|
||||
# uint16_t, ...) is emitted with the right type instead of whatever safe_exp() infers.
|
||||
(option.field, option.cpp_type(config[option.conf_key]))
|
||||
for option in _command_options(direction)
|
||||
if option.conf_key in config
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def register_templatable_command_options(
|
||||
var: MockObj, config: ConfigType, args: TemplateArgsType, direction: str
|
||||
) -> None:
|
||||
|
||||
@@ -157,6 +157,10 @@ _ACTION_BASE_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
# The write codes recognised by modbus::helpers::is_function_code_write() - keep in sync. 0x17
|
||||
# (read/write multiple) is included: it mutates, so the hub treats it as a write despite its read half.
|
||||
_WRITE_FUNCTION_CODES = frozenset({0x05, 0x06, 0x0F, 0x10, 0x16, 0x17})
|
||||
|
||||
|
||||
def _no_continuous_on_write(config: ConfigType) -> ConfigType:
|
||||
"""Reject `continuous: true` on a static write PDU: continuous polling only applies to reads.
|
||||
@@ -166,7 +170,9 @@ def _no_continuous_on_write(config: ConfigType) -> ConfigType:
|
||||
if (
|
||||
isinstance(pdu, list)
|
||||
and config.get(CONF_CONTINUOUS) is True
|
||||
and modbus.is_function_code_write(pdu[0])
|
||||
# Masking the exception bit (0x90 -> 0x10) makes this check stricter than the runtime hub,
|
||||
# whose classify() treats an exception-flagged code as a read and leaves continuous in place.
|
||||
and pdu[0] & 0x7F in _WRITE_FUNCTION_CODES
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_CONTINUOUS}: true' does not apply to a write PDU (function code "
|
||||
|
||||
@@ -11,14 +11,7 @@ from esphome.components.modbus.helpers import (
|
||||
EntityType,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_CONTINUOUS,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_NAME,
|
||||
CONF_OFFSET,
|
||||
)
|
||||
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_NAME, CONF_OFFSET
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_helpers import logging
|
||||
import esphome.final_validate as fv
|
||||
@@ -132,7 +125,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
|
||||
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
|
||||
**modbus.command_options_schema(direction="read"),
|
||||
cv.Optional(
|
||||
CONF_SERVER_REGISTERS,
|
||||
): cv.invalid(
|
||||
@@ -242,35 +234,6 @@ def migrate_custom_command(config: ConfigType) -> None:
|
||||
del config[CONF_CUSTOM_COMMAND]
|
||||
|
||||
|
||||
def _reject_continuous_write_custom_pdu(config: ConfigType) -> None:
|
||||
"""Final-validate: a custom_pdu whose function code writes (e.g. 0x17 read/write-multiple) cannot be
|
||||
polled continuously - the hub ignores continuous for mutating codes and would warn on every update
|
||||
while that range silently does not stream. Reject the combination instead. Runs after
|
||||
migrate_custom_command, so it sees custom_pdu whether written directly or migrated from
|
||||
custom_command."""
|
||||
pdu = config.get(CONF_CUSTOM_PDU)
|
||||
if pdu is None or not modbus.is_function_code_write(pdu[0]):
|
||||
return
|
||||
fconf = fv.full_config.get()
|
||||
path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1]
|
||||
controller = fconf.get_config_for_path(path)
|
||||
if controller.get(CONF_CONTINUOUS) is True:
|
||||
raise cv.Invalid(
|
||||
f"a '{CONF_CUSTOM_PDU}' with a write function code (0x{pdu[0] & 0x7F:02X}) can't be polled "
|
||||
f"continuously: the hub ignores 'continuous' for mutating codes. Remove 'continuous: true' "
|
||||
f"from the '{controller[CONF_ID]}' modbus_controller, or use a read function code.",
|
||||
[CONF_CUSTOM_PDU],
|
||||
)
|
||||
|
||||
|
||||
def validate_custom_pdu_item(config: ConfigType) -> None:
|
||||
"""Final-validate for the read platforms that accept custom_pdu (sensor, binary_sensor,
|
||||
text_sensor): migrate the deprecated custom_command, then reject a write-coded custom_pdu under a
|
||||
continuously-polling controller."""
|
||||
migrate_custom_command(config)
|
||||
_reject_continuous_write_custom_pdu(config)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
modbus.final_validate_modbus_device("modbus_controller", role="client")(config)
|
||||
|
||||
@@ -351,11 +314,6 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
|
||||
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
|
||||
cg.add(
|
||||
var.set_read_options(
|
||||
modbus.command_options_expression(config, direction="read")
|
||||
)
|
||||
)
|
||||
await register_modbus_device(var, config)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ from .. import (
|
||||
ModbusItemBaseSchema,
|
||||
SensorItem,
|
||||
add_modbus_base_properties,
|
||||
migrate_custom_command,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
validate_modbus_register,
|
||||
)
|
||||
from ..const import (
|
||||
@@ -40,7 +40,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_modbus_register,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
FINAL_VALIDATE_SCHEMA = migrate_custom_command
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -167,7 +167,6 @@ void ModbusController::queue_command(ModbusCommandItem command) {
|
||||
this->one_shot_command_items_.push_back(make_unique<ModbusCommandItem>(std::move(command)));
|
||||
// A refused frame gets no terminal callback (see the hub contract), so reclaim the item here.
|
||||
auto &item = this->one_shot_command_items_.back();
|
||||
// We intentionally do not pass read_options_ here, because one-shot commands are usually writes, and are non-polling.
|
||||
if (!item->send()) {
|
||||
// The caller (e.g. a write entity) has usually already published optimistically - surface the loss.
|
||||
ESP_LOGW(TAG, "Command refused by hub: type=0x%X address=0x%X", static_cast<uint8_t>(item->register_type()),
|
||||
@@ -204,9 +203,7 @@ void ModbusController::update() {
|
||||
ESP_LOGV(TAG, "Module offline - retrying");
|
||||
this->cmd_non_responses_ = 0; // allow the probe through can_send()
|
||||
for (auto &cmd : this->polling_command_items_) {
|
||||
// Probes carry the read-side options too, so a recovering device resumes streaming on the
|
||||
// probe itself rather than waiting for the next update_interval.
|
||||
if (!cmd.send(this->read_options_)) {
|
||||
if (!cmd.send()) {
|
||||
ESP_LOGD(TAG, "Probe refused by hub for range 0x%X", cmd.register_address());
|
||||
}
|
||||
}
|
||||
@@ -220,11 +217,9 @@ void ModbusController::update() {
|
||||
if (this->can_send()) {
|
||||
for (auto &cmd : this->polling_command_items_) {
|
||||
ESP_LOGVV(TAG, "Updating range 0x%X", cmd.register_address());
|
||||
// read_options_ carries the controller's continuous flag (the offline probe above sends it too).
|
||||
// A refusal is already logged by the hub; note the affected range for controller-level diagnostics.
|
||||
if (!cmd.send(this->read_options_)) {
|
||||
if (!cmd.send())
|
||||
ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", cmd.register_address());
|
||||
}
|
||||
}
|
||||
}
|
||||
this->update_counter_++;
|
||||
@@ -500,18 +495,16 @@ ModbusCommandItem ModbusCommandItem::create_custom_command(
|
||||
return cmd;
|
||||
}
|
||||
|
||||
bool ModbusCommandItem::send(modbus::CommandOptions options) {
|
||||
// Options pass straight through to the hub
|
||||
bool ModbusCommandItem::send() {
|
||||
bool accepted;
|
||||
if (this->custom_pdu_ != nullptr) {
|
||||
// Custom polling command: send the sensor's ready-made PDU (function code + data, no address byte)
|
||||
// to this controller's own device address; the hub prepends the address and appends the CRC.
|
||||
accepted = modbus::ModbusClientDevice::queue_pdu(std::span<const uint8_t>(*this->custom_pdu_), options);
|
||||
accepted = modbus::ModbusClientDevice::queue_pdu(std::span<const uint8_t>(*this->custom_pdu_));
|
||||
} else if (this->function_code_ != FunctionCode::CUSTOM) {
|
||||
accepted = this->queue_pdu(modbus::helpers::create_client_pdu(
|
||||
this->function_code_, this->start_address_, this->register_count_,
|
||||
this->payload.empty() ? nullptr : this->payload.data(), this->payload.size()),
|
||||
options);
|
||||
this->function_code_, this->start_address_, this->register_count_,
|
||||
this->payload.empty() ? nullptr : this->payload.data(), this->payload.size()));
|
||||
} else {
|
||||
// Factory custom command: payload holds a complete raw frame (address + PDU). Send the PDU to the
|
||||
// frame's own address (which may differ from this controller's); the hub appends the CRC and routes
|
||||
@@ -521,7 +514,7 @@ bool ModbusCommandItem::send(modbus::CommandOptions options) {
|
||||
ESP_LOGW(TAG, "Empty custom command frame, not sent");
|
||||
accepted = false;
|
||||
} else {
|
||||
accepted = this->parent_->queue_pdu(frame[0], frame.subspan(1), this, options);
|
||||
accepted = this->parent_->queue_pdu(frame[0], frame.subspan(1), this);
|
||||
}
|
||||
}
|
||||
// The on_command_sent trigger fires from on_sent() when the frame actually reaches the wire.
|
||||
|
||||
@@ -284,9 +284,7 @@ class ModbusCommandItem : public modbus::ModbusClientDevice {
|
||||
/// Queue this command's frame on the hub. Returns false when refused, in which case no callback ever comes.
|
||||
/// The item is the hub device, so it must stay alive until its terminal callback; a destroyed item's
|
||||
/// pending frame is silently retired.
|
||||
/// Options pass straight through to the hub; the polling path passes the controller's read-side
|
||||
/// options so reads re-queue after each success, one-shot commands keep the default.
|
||||
bool send(modbus::CommandOptions options = {});
|
||||
bool send();
|
||||
|
||||
/// factory methods
|
||||
/** Create modbus read command
|
||||
@@ -454,10 +452,6 @@ class ModbusController final : public PollingComponent {
|
||||
void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
|
||||
/// get how many times a command will be (re)sent if no response is received
|
||||
uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
|
||||
/// called by esphome generated code with the read-side command options applied to every poll
|
||||
void set_read_options(modbus::CommandOptions options) { this->read_options_ = options; }
|
||||
/// the read-side command options applied to every poll
|
||||
const modbus::CommandOptions &read_options() const { return this->read_options_; }
|
||||
|
||||
protected:
|
||||
/// parse sensormap_ and create range of sequential addresses
|
||||
@@ -503,8 +497,6 @@ class ModbusController final : public PollingComponent {
|
||||
uint16_t offline_skip_updates_{0};
|
||||
/// How many times we will retry a command if we get no response
|
||||
uint8_t max_cmd_retries_{4};
|
||||
/// read-side command options applied to every poll
|
||||
modbus::CommandOptions read_options_{};
|
||||
/// Command sent callback
|
||||
CallbackManager<void(int, int)> command_sent_callback_{};
|
||||
/// Server online callback
|
||||
|
||||
@@ -18,9 +18,9 @@ from .. import (
|
||||
ModbusItemBaseSchema,
|
||||
SensorItem,
|
||||
add_modbus_base_properties,
|
||||
migrate_custom_command,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
)
|
||||
from ..const import (
|
||||
CONF_BITMASK,
|
||||
@@ -86,7 +86,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_modbus_number,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
FINAL_VALIDATE_SCHEMA = migrate_custom_command
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -8,9 +8,9 @@ from .. import (
|
||||
ModbusItemBaseSchema,
|
||||
SensorItem,
|
||||
add_modbus_base_properties,
|
||||
migrate_custom_command,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
validate_modbus_register,
|
||||
)
|
||||
from ..const import (
|
||||
@@ -44,7 +44,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_modbus_register,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
FINAL_VALIDATE_SCHEMA = migrate_custom_command
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -8,9 +8,9 @@ from .. import (
|
||||
ModbusItemBaseSchema,
|
||||
SensorItem,
|
||||
add_modbus_base_properties,
|
||||
migrate_custom_command,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
validate_modbus_register,
|
||||
)
|
||||
from ..const import (
|
||||
@@ -45,7 +45,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_modbus_register,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
FINAL_VALIDATE_SCHEMA = migrate_custom_command
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -8,9 +8,9 @@ from .. import (
|
||||
ModbusItemBaseSchema,
|
||||
SensorItem,
|
||||
add_modbus_base_properties,
|
||||
migrate_custom_command,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
validate_modbus_register,
|
||||
)
|
||||
from ..const import (
|
||||
@@ -55,7 +55,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_modbus_register,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
FINAL_VALIDATE_SCHEMA = migrate_custom_command
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import platformdirs
|
||||
|
||||
@@ -12,8 +13,9 @@ import esphome.config_validation as cv
|
||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.framework_helpers import (
|
||||
archive_extract_all,
|
||||
create_venv,
|
||||
download_and_extract,
|
||||
download_from_mirrors,
|
||||
get_python_env_executable_path,
|
||||
rmdir,
|
||||
run_command_ok,
|
||||
@@ -344,37 +346,34 @@ def check_and_install() -> None:
|
||||
if not sentinel.exists():
|
||||
rmdir(toolchains_dir, msg=f"Clean up {TOOLCHAIN_VERSION} toolchain environment")
|
||||
sysname, machine, extension = _get_toolchain_platform_info()
|
||||
substitutions = {
|
||||
"VERSION": TOOLCHAIN_VERSION,
|
||||
"sysname": sysname,
|
||||
"machine": machine,
|
||||
"extension": extension,
|
||||
}
|
||||
# Downloaded next to the destination (not a temp file) so an
|
||||
# interrupted download's .part file resumes on the next run.
|
||||
for mirrors, extract_dir, what, slug in (
|
||||
(SDK_NG_MINIMAL_MIRRORS, toolchains_dir, "Zephyr SDK minimal", "minimal"),
|
||||
(
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
_LOGGER.info("Downloading Zephyr SDK %s minimal ...", TOOLCHAIN_VERSION)
|
||||
download_from_mirrors(
|
||||
SDK_NG_MINIMAL_MIRRORS,
|
||||
{
|
||||
"VERSION": TOOLCHAIN_VERSION,
|
||||
"sysname": sysname,
|
||||
"machine": machine,
|
||||
"extension": extension,
|
||||
},
|
||||
tmp.file,
|
||||
)
|
||||
archive_extract_all(tmp.file, toolchains_dir, progress_header="Extracting")
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
_LOGGER.info("Downloading %s toolchain ...", TOOLCHAIN_VERSION)
|
||||
download_from_mirrors(
|
||||
SDK_NG_TOOLCHAIN_MIRRORS,
|
||||
{
|
||||
"VERSION": TOOLCHAIN_VERSION,
|
||||
"sysname": sysname,
|
||||
"machine": machine,
|
||||
"extension": extension,
|
||||
},
|
||||
tmp.file,
|
||||
)
|
||||
archive_extract_all(
|
||||
tmp.file,
|
||||
toolchains_dir / "arm-zephyr-eabi",
|
||||
"toolchain",
|
||||
"toolchain",
|
||||
),
|
||||
):
|
||||
_LOGGER.info("Downloading %s %s ...", TOOLCHAIN_VERSION, what)
|
||||
download_and_extract(
|
||||
mirrors,
|
||||
substitutions,
|
||||
toolchains_dir.with_name(f"{toolchains_dir.name}.{slug}.archive"),
|
||||
extract_dir,
|
||||
progress_header="Extracting",
|
||||
)
|
||||
# Best-effort prune of resume leftovers, including a previous
|
||||
# TOOLCHAIN_VERSION's orphans; the SDK archives are hundreds of MB.
|
||||
# A locked file must not discard the just-completed install.
|
||||
for leftover in toolchains_dir.parent.glob("*.archive.part*"):
|
||||
try:
|
||||
leftover.unlink()
|
||||
except OSError as err:
|
||||
_LOGGER.debug("Could not remove %s: %s", leftover, err)
|
||||
sentinel.touch()
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
static const char *const TAG = "online_image";
|
||||
static const char *const CONTENT_TYPE_HEADER_NAME = "content-type";
|
||||
@@ -63,11 +62,30 @@ void OnlineImage::update() {
|
||||
headers.push_back({IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_});
|
||||
}
|
||||
|
||||
// Add Accept header based on image format
|
||||
const char *accept_mime_type;
|
||||
runtime_image::ImageFormat format = this->get_format();
|
||||
// Accept: "<mime>,*/*;q=0.8"; 32 covers the longest MIME type plus the suffix
|
||||
char accept_header[32];
|
||||
snprintf(accept_header, sizeof(accept_header), "%s,*/*;q=0.8", runtime_image::get_mime_type_for_format(format));
|
||||
headers.push_back({"Accept", accept_header});
|
||||
switch (format) {
|
||||
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||
case runtime_image::BMP:
|
||||
accept_mime_type = "image/bmp,*/*;q=0.8";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
case runtime_image::JPEG:
|
||||
accept_mime_type = "image/jpeg,*/*;q=0.8";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||
case runtime_image::PNG:
|
||||
accept_mime_type = "image/png,*/*;q=0.8";
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
accept_mime_type = "image/*,*/*;q=0.8";
|
||||
break;
|
||||
}
|
||||
headers.push_back({"Accept", accept_mime_type});
|
||||
|
||||
// User headers last so they can override any of the above
|
||||
for (auto &header : this->request_headers_) {
|
||||
@@ -104,18 +122,32 @@ void OnlineImage::update() {
|
||||
|
||||
if (format == runtime_image::AUTO) {
|
||||
// Try to auto-detect format from Content-Type header
|
||||
auto content_type = this->downloader_->get_response_header(CONTENT_TYPE_HEADER_NAME);
|
||||
ESP_LOGV(TAG, "Content-Type: %s", content_type.c_str());
|
||||
auto mime_format = esphome::runtime_image::get_format_for_mime_type(content_type.c_str());
|
||||
if (mime_format.has_value()) {
|
||||
format = *mime_format;
|
||||
auto content_type_header = this->downloader_->get_response_header(CONTENT_TYPE_HEADER_NAME);
|
||||
const char *content_type = content_type_header.c_str();
|
||||
ESP_LOGV(TAG, "Content-Type: %s", content_type);
|
||||
// Includes aliases seen from real servers (older IIS, CDNs, S3)
|
||||
if (str_contains_ignore_case(content_type, "image/bmp") ||
|
||||
str_contains_ignore_case(content_type, "image/x-ms-bmp") ||
|
||||
str_contains_ignore_case(content_type, "image/x-bmp")) {
|
||||
format = runtime_image::BMP;
|
||||
} else if (str_contains_ignore_case(content_type, "image/jpeg") ||
|
||||
str_contains_ignore_case(content_type, "image/jpg")) {
|
||||
format = runtime_image::JPEG;
|
||||
} else if (str_contains_ignore_case(content_type, "image/png") ||
|
||||
str_contains_ignore_case(content_type, "image/x-png")) {
|
||||
format = runtime_image::PNG;
|
||||
} else if (str_contains_ignore_case(content_type, "image/")) {
|
||||
ESP_LOGW(TAG, "Unsupported image type: '%s'", content_type);
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else {
|
||||
if (content_type.empty()) {
|
||||
ESP_LOGE(TAG, "Server sent no Content-Type header; cannot determine image format. Set `format:` explicitly");
|
||||
} else if (str_contains_ignore_case(content_type.c_str(), "image/")) {
|
||||
ESP_LOGE(TAG, "Image format '%s' not supported.", content_type.c_str());
|
||||
// TODO: implement auto-detection in runtime_image by sniffing the first few bytes of the image data
|
||||
if (content_type_header.empty()) {
|
||||
ESP_LOGW(TAG, "Server sent no Content-Type header; cannot determine image format. Set `format:` explicitly");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Server did not return an image (Content-Type: '%s')", content_type.c_str());
|
||||
ESP_LOGE(TAG, "Could not determine image format from Content-Type: '%s'. Set `format:` explicitly",
|
||||
content_type);
|
||||
}
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::output {
|
||||
|
||||
static const char *const TAG = "output.automation";
|
||||
|
||||
} // namespace esphome::output
|
||||
@@ -9,7 +9,7 @@ from esphome.types import ConfigType
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
preferences_ns = cg.esphome_ns.namespace("preferences")
|
||||
IntervalSyncer = preferences_ns.class_("IntervalSyncer", cg.PollingComponent)
|
||||
IntervalSyncer = preferences_ns.class_("IntervalSyncer", cg.Component)
|
||||
|
||||
CONF_FLASH_WRITE_INTERVAL = "flash_write_interval"
|
||||
CONF_RTC_STORAGE = "rtc_storage"
|
||||
@@ -31,7 +31,10 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
write_interval = config[CONF_FLASH_WRITE_INTERVAL]
|
||||
cg.add(var.set_update_interval(write_interval))
|
||||
if write_interval.total_milliseconds == 0:
|
||||
cg.add_define("USE_PREFERENCES_SYNC_EVERY_LOOP")
|
||||
else:
|
||||
cg.add(var.set_write_interval(write_interval))
|
||||
if config.get(CONF_RTC_STORAGE):
|
||||
preferences.request_rtc_storage()
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -2,19 +2,26 @@
|
||||
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/component.h"
|
||||
// Include for ESPDEPRECATED, Remove before 2027.3.0
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::preferences {
|
||||
|
||||
class IntervalSyncer final : public PollingComponent {
|
||||
class IntervalSyncer final : public Component {
|
||||
public:
|
||||
// Remove before 2027.3.0
|
||||
ESPDEPRECATED("Use set_update_interval() instead. Removed in 2027.3.0", "2026.9.0")
|
||||
void set_write_interval(uint32_t write_interval) { this->set_update_interval(write_interval); }
|
||||
void update() override { global_preferences->sync(); }
|
||||
#ifdef USE_PREFERENCES_SYNC_EVERY_LOOP
|
||||
void loop() override { global_preferences->sync(); }
|
||||
#else
|
||||
void set_write_interval(uint32_t write_interval) { this->write_interval_ = write_interval; }
|
||||
void setup() override {
|
||||
this->set_interval(this->write_interval_, []() { global_preferences->sync(); });
|
||||
}
|
||||
#endif
|
||||
void on_shutdown() override { global_preferences->sync(); }
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
#ifndef USE_PREFERENCES_SYNC_EVERY_LOOP
|
||||
protected:
|
||||
uint32_t write_interval_{60000};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::preferences
|
||||
|
||||
@@ -4,15 +4,14 @@ Scan modes:
|
||||
continuous: true — scan runs forever; never stops automatically.
|
||||
continuous: false — a started scan runs for `duration`, then stops. The first
|
||||
start is external too; nothing starts a non-continuous
|
||||
scan on boot — use the rp2_ble_tracker.start_scan action
|
||||
(e.g. from api: on_client_connected:).
|
||||
scan on boot. Until start/stop automation actions land
|
||||
(follow-up PR), starting means a lambda:
|
||||
`id(my_tracker).start_scan();`.
|
||||
"""
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import ble_device_base, ota, rp2040_ble
|
||||
from esphome.components.ble_device_base import automation as ble_automation
|
||||
from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CONF_WINDOW
|
||||
from esphome.components.const import CONF_SCAN_PARAMETERS, CONF_WINDOW
|
||||
from esphome.components.rp2040_ble import CONF_RP2040_BLE_ID
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -21,13 +20,7 @@ from esphome.const import (
|
||||
CONF_DURATION,
|
||||
CONF_ID,
|
||||
CONF_INTERVAL,
|
||||
CONF_MANUFACTURER_ID,
|
||||
CONF_ON_BLE_ADVERTISE,
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
||||
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
|
||||
CONF_SERVICE_UUID,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["rp2"]
|
||||
@@ -41,14 +34,6 @@ RP2BLETracker = rp2_ble_tracker_ns.class_(
|
||||
"RP2BLETracker", ble_device_base.BLEHub, cg.Component
|
||||
)
|
||||
|
||||
StartScanAction = rp2_ble_tracker_ns.class_("StartScanAction", automation.Action)
|
||||
StopScanAction = rp2_ble_tracker_ns.class_("StopScanAction", automation.Action)
|
||||
|
||||
ESPBTAdvertiseTrigger = ble_automation.ESPBTAdvertiseTrigger
|
||||
BLEServiceDataAdvertiseTrigger = ble_automation.BLEServiceDataAdvertiseTrigger
|
||||
BLEManufacturerDataAdvertiseTrigger = ble_automation.BLEManufacturerDataAdvertiseTrigger
|
||||
BLEEndOfScanTrigger = ble_automation.BLEEndOfScanTrigger
|
||||
|
||||
|
||||
# interval defaults to 100 ms with the shared 30 ms window, a 30 % duty cycle —
|
||||
# the same defaults as bk72xx_ble_tracker, leaving the radio mostly free for
|
||||
@@ -63,24 +48,6 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(): cv.declare_id(RP2BLETracker),
|
||||
cv.GenerateID(CONF_RP2040_BLE_ID): cv.use_id(rp2040_ble.RP2040BLE),
|
||||
cv.Optional(CONF_SCAN_PARAMETERS, default={}): SCAN_PARAMETERS_SCHEMA,
|
||||
cv.Optional(CONF_ON_BLE_ADVERTISE): ble_automation.advertise_trigger_schema(
|
||||
ESPBTAdvertiseTrigger
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_ON_BLE_SERVICE_DATA_ADVERTISE
|
||||
): ble_automation.uuid_trigger_schema(
|
||||
BLEServiceDataAdvertiseTrigger,
|
||||
{cv.Required(CONF_SERVICE_UUID): ble_device_base.bt_uuid},
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE
|
||||
): ble_automation.uuid_trigger_schema(
|
||||
BLEManufacturerDataAdvertiseTrigger,
|
||||
{cv.Required(CONF_MANUFACTURER_ID): ble_device_base.bt_uuid},
|
||||
),
|
||||
cv.Optional(CONF_ON_SCAN_END): ble_automation.scan_end_trigger_schema(
|
||||
BLEEndOfScanTrigger
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@@ -109,71 +76,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_scan_window(ble_device_base.to_ble_units(scan[CONF_WINDOW])))
|
||||
cg.add(var.set_scan_duration(scan[CONF_DURATION].total_milliseconds))
|
||||
cg.add(var.set_scan_active(scan[CONF_ACTIVE]))
|
||||
cg.add(var.set_configured_continuous(scan[CONF_CONTINUOUS]))
|
||||
|
||||
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
|
||||
await ble_automation.advertise_trigger_to_code(conf, var)
|
||||
|
||||
for trigger_key, uuid_key, setter_prefix in (
|
||||
(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_SERVICE_UUID, "set_service_uuid"),
|
||||
(
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
||||
CONF_MANUFACTURER_ID,
|
||||
"set_manufacturer_uuid",
|
||||
),
|
||||
):
|
||||
for conf in config.get(trigger_key, []):
|
||||
await ble_automation.uuid_trigger_to_code(
|
||||
conf, var, uuid_key, setter_prefix
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_SCAN_END, []):
|
||||
await ble_automation.scan_end_trigger_to_code(conf, var)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"rp2_ble_tracker.start_scan",
|
||||
StartScanAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(RP2BLETracker),
|
||||
cv.Optional(CONF_CONTINUOUS): cv.templatable(cv.boolean),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def start_scan_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: list,
|
||||
) -> cg.MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
if (continuous := config.get(CONF_CONTINUOUS)) is not None:
|
||||
template_ = await cg.templatable(continuous, args, cg.bool_)
|
||||
cg.add(var.set_continuous(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"rp2_ble_tracker.stop_scan",
|
||||
StopScanAction,
|
||||
automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(RP2BLETracker),
|
||||
}
|
||||
)
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def stop_scan_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: list,
|
||||
) -> cg.MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
cg.add(var.set_scan_continuous(scan[CONF_CONTINUOUS]))
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// Scan-control actions for rp2_ble_tracker. The automation triggers are the
|
||||
// neutral ble_device_base classes (ble_device_base/automation.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_RP2
|
||||
|
||||
#include "rp2_ble_tracker.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::rp2_ble_tracker {
|
||||
|
||||
template<typename... Ts> class StartScanAction final : public Action<Ts...>, public Parented<RP2BLETracker> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(bool, continuous)
|
||||
void play(const Ts &...x) override {
|
||||
// With continuous: set, the action wins. Without it, the configured value
|
||||
// is used - stop_scan() clears the runtime flag permanently, so a bare
|
||||
// stop_scan/start_scan pair would otherwise never resume continuous mode.
|
||||
const bool want =
|
||||
this->continuous_.has_value() ? this->continuous_.value(x...) : this->parent_->configured_continuous();
|
||||
if (this->parent_->scan_running()) {
|
||||
// Same mode on a running scan is a no-op (esp32 parity): re-anchoring
|
||||
// the duration window here would let a repeated action keep a one-shot
|
||||
// scan alive forever. A real mode switch re-anchors so a change to
|
||||
// one-shot runs a full duration from now.
|
||||
if (want != this->parent_->scan_continuous()) {
|
||||
this->parent_->set_scan_continuous(want);
|
||||
this->parent_->restart_scan_duration();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this->parent_->set_scan_continuous(want);
|
||||
this->parent_->start_scan();
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class StopScanAction final : public Action<Ts...>, public Parented<RP2BLETracker> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->stop_scan(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::rp2_ble_tracker
|
||||
|
||||
#endif // USE_RP2
|
||||
@@ -11,8 +11,10 @@ namespace esphome::rp2_ble_tracker {
|
||||
|
||||
static const char *const TAG = "rp2_ble_tracker";
|
||||
|
||||
// Floor between controller start attempts; insurance against a failing
|
||||
// scan_start() being retried every loop.
|
||||
// Minimum interval between scan start attempts on an active stack. The
|
||||
// controller start has no failure mode once HCI is WORKING, so this fires at
|
||||
// most once per enable cycle today; the floor is insurance against a future
|
||||
// scan_start() failure being retried every main-loop iteration.
|
||||
static constexpr uint32_t SCAN_START_RETRY_MS = 1000;
|
||||
|
||||
// One BLE scan unit in milliseconds; the controller programs interval/window in these units.
|
||||
@@ -30,8 +32,7 @@ void RP2BLETracker::setup() {
|
||||
// the OTA download on the shared CYW43 radio. Mirrors esp32_ble_tracker.
|
||||
ota::get_global_ota_callback()->add_global_state_listener(this);
|
||||
#endif
|
||||
// An on_boot start_scan runs before setup(); parking here would strand it.
|
||||
if (!this->scan_continuous_ && !this->scan_running_ && !this->pending_start_) {
|
||||
if (!this->scan_continuous_) {
|
||||
// Nothing to do until an external start_scan(); the loop is re-enabled there.
|
||||
this->disable_loop();
|
||||
}
|
||||
@@ -40,21 +41,12 @@ void RP2BLETracker::setup() {
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
void RP2BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
|
||||
if (state == ota::OTA_STARTED) {
|
||||
// Set before stop_scan(): its on_scan_end automations run synchronously and
|
||||
// may call start_scan(), which must defer instead of resuming the radio.
|
||||
this->ota_in_progress_ = true;
|
||||
this->scan_continuous_before_ota_ = this->scan_continuous_;
|
||||
// A one-shot scan counts as pending when it is running, latched, or still
|
||||
// retrying its start (loop enabled); captured before stop_scan() parks it.
|
||||
this->scan_pending_before_ota_ =
|
||||
!this->scan_continuous_ && (this->scan_running_ || this->pending_start_ || this->is_in_loop_state());
|
||||
// The pause's own stop is not a user stop, so it must not clear the latches
|
||||
// captured just above.
|
||||
this->ota_pausing_ = true;
|
||||
// A one-shot scan counts as pending when it is running or still retrying
|
||||
// its start (loop enabled); captured before stop_scan() disables the loop.
|
||||
this->scan_pending_before_ota_ = !this->scan_continuous_ && (this->scan_running_ || this->is_in_loop_state());
|
||||
this->stop_scan();
|
||||
this->ota_pausing_ = false;
|
||||
} else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
|
||||
this->ota_in_progress_ = false;
|
||||
// On success the device reboots, so restore only on a failed/aborted update;
|
||||
// loop()'s retry branch restarts the scan on its next iteration.
|
||||
if (this->scan_continuous_before_ota_) {
|
||||
@@ -62,7 +54,9 @@ void RP2BLETracker::on_ota_global_state(ota::OTAState state, float progress, uin
|
||||
this->scan_continuous_ = true;
|
||||
this->enable_loop();
|
||||
}
|
||||
// A failed OTA does not reboot, so nothing else would restart a one-shot.
|
||||
// A one-shot scan interrupted by the OTA resumes for a fresh duration
|
||||
// rather than silently staying idle — an OTA failure does not reboot, so
|
||||
// nothing external would restart it.
|
||||
if (this->scan_pending_before_ota_) {
|
||||
this->scan_pending_before_ota_ = false;
|
||||
this->enable_loop();
|
||||
@@ -72,34 +66,27 @@ void RP2BLETracker::on_ota_global_state(ota::OTAState state, float progress, uin
|
||||
#endif // USE_OTA_STATE_LISTENER
|
||||
|
||||
void RP2BLETracker::loop() {
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Keeps "no radio during an OTA" local instead of emergent from the
|
||||
// parking sites.
|
||||
if (this->ota_in_progress_)
|
||||
return;
|
||||
#endif
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->pending_start_ && this->parent_->is_active()) {
|
||||
// Latched start, applied once the stack is ACTIVE; earlier attempts would
|
||||
// fail and arm the retry floor for nothing.
|
||||
this->pending_start_ = false;
|
||||
if (!this->scan_running_)
|
||||
this->start_scan_();
|
||||
}
|
||||
// Deliver held scannable advertisements whose scan response never arrived —
|
||||
// unmerged after the merger's timeout.
|
||||
if (!this->merger_.empty())
|
||||
this->merger_.sweep(now);
|
||||
if (this->scan_running_ && !this->parent_->is_active()) {
|
||||
// Stack disabled underneath us; reconcile so the retry branch takes over.
|
||||
// The controller was disabled underneath us (e.g. a lambda calling
|
||||
// rp2040_ble's disable()); the scan died with the stack. Reconcile so the
|
||||
// retry branch below takes over once the user re-enables the stack.
|
||||
this->scan_running_ = false;
|
||||
this->fire_scan_end_();
|
||||
}
|
||||
if (!this->scan_running_) {
|
||||
// Should be scanning but is not: continuous until the start succeeds,
|
||||
// one-shot only between start_scan() and a successful controller start.
|
||||
// A scan should be running but is not: continuous mode is always in this
|
||||
// state until the start succeeds, and non-continuous mode only reaches
|
||||
// here between start_scan() and a successful controller start, because
|
||||
// stop_scan_() disables the loop otherwise.
|
||||
if (!this->parent_->is_active()) {
|
||||
// Stack not up: scan_start() cannot succeed yet.
|
||||
// Stack not up (still booting, or the user called disable()) —
|
||||
// scan_start() cannot succeed, so there is nothing to attempt; scanning
|
||||
// starts on the first iteration after HCI reaches WORKING.
|
||||
return;
|
||||
}
|
||||
if (now - this->last_scan_start_attempt_ >= SCAN_START_RETRY_MS) {
|
||||
@@ -120,7 +107,7 @@ void RP2BLETracker::loop() {
|
||||
|
||||
// Non-continuous mode: run for scan_duration_ ms, then stop and fire on_scan_end.
|
||||
// Restart is driven externally (e.g. api: on_client_connected:).
|
||||
if (now - this->scan_start_time_ >= this->scan_duration_) {
|
||||
if (now - this->scan_period_start_ >= this->scan_duration_) {
|
||||
this->stop_scan_();
|
||||
}
|
||||
}
|
||||
@@ -139,20 +126,24 @@ void RP2BLETracker::dump_config() {
|
||||
YESNO(this->scan_continuous_));
|
||||
}
|
||||
|
||||
// Core spec advertising report event types (BTstack headers stay out of this
|
||||
// TU). ADV_IND and ADV_SCAN_IND are the scannable ones.
|
||||
// GAP advertising event types as BTstack reports them (Core spec advertising
|
||||
// report event types; the tracker deliberately does not include BTstack
|
||||
// headers). ADV_IND and ADV_SCAN_IND are the scannable types.
|
||||
static constexpr uint8_t ADV_EVENT_TYPE_ADV_IND = 0;
|
||||
static constexpr uint8_t ADV_EVENT_TYPE_ADV_SCAN_IND = 2;
|
||||
static constexpr uint8_t ADV_EVENT_TYPE_SCAN_RSP = 4;
|
||||
|
||||
// BTstack delivers the pair as separate reports; the merger holds a scannable
|
||||
// advertisement until its response arrives.
|
||||
// Demux advertisements vs scan responses into the shared merger: BTstack
|
||||
// delivers the pair as separate reports; a scannable advertisement is held
|
||||
// until its scan response arrives and delivered as one merged frame.
|
||||
void RP2BLETracker::on_scan_report(const rp2040_ble::BLEScanReport &report) {
|
||||
if (report.adv_event_type == ADV_EVENT_TYPE_SCAN_RSP) {
|
||||
this->merger_.submit_scan_rsp(report.mac, report.rssi, report.addr_type, report.data, report.data_len);
|
||||
return;
|
||||
}
|
||||
// Only while an active scan runs: nothing sweeps the merger after a stop.
|
||||
// Stash only while an active scan runs: a passive scan never gets a
|
||||
// response, and after a stop nothing would sweep the merger, so a late
|
||||
// report would surface minutes later as a fresh advertisement.
|
||||
if (this->scan_running_ && this->scan_active_ &&
|
||||
(report.adv_event_type == ADV_EVENT_TYPE_ADV_IND || report.adv_event_type == ADV_EVENT_TYPE_ADV_SCAN_IND)) {
|
||||
this->merger_.stash_adv(report.mac, report.rssi, report.addr_type, report.data, report.data_len,
|
||||
@@ -166,49 +157,20 @@ void RP2BLETracker::on_scan_report(const rp2040_ble::BLEScanReport &report) {
|
||||
void RP2BLETracker::start_scan() {
|
||||
// Mirrors esp32_ble_tracker::start_scan(): caller sets scan_continuous_ via
|
||||
// set_scan_continuous() first, then calls start_scan() to begin scanning.
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
if (this->ota_in_progress_) {
|
||||
// Defer to the post-OTA resume path, carrying the requested mode. Not
|
||||
// while ota_pausing_: scan_continuous_ is an artefact of the pause's own
|
||||
// stop there, not intent.
|
||||
if (!this->ota_pausing_) {
|
||||
this->scan_continuous_before_ota_ = this->scan_continuous_;
|
||||
this->scan_pending_before_ota_ = !this->scan_continuous_;
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->enable_loop();
|
||||
if (!this->is_ready() || !this->parent_->is_active()) {
|
||||
// Pre-setup or stack not ACTIVE: latch, loop() applies it.
|
||||
this->pending_start_ = true;
|
||||
return;
|
||||
}
|
||||
// bk72xx force semantics: a user start jumps the floor only while the
|
||||
// controller is healthy. loop()'s retry branch picks the request up.
|
||||
if (this->last_start_failed_ &&
|
||||
App.get_loop_component_start_time() - this->last_scan_start_attempt_ < SCAN_START_RETRY_MS) {
|
||||
return;
|
||||
}
|
||||
this->start_scan_();
|
||||
}
|
||||
|
||||
void RP2BLETracker::restart_scan_duration() {
|
||||
if (!this->scan_running_)
|
||||
return; // start_scan_() anchors the clock itself on the next real start
|
||||
// One-shot clock only (bk72xx parity); re-anchoring the period would let
|
||||
// repeated actions starve on_scan_end. Same clock as loop()'s now.
|
||||
this->scan_start_time_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
bool RP2BLETracker::request_scan_mode(bool active) {
|
||||
if (this->scan_active_ == active)
|
||||
return true;
|
||||
this->scan_active_ = active;
|
||||
// V: the proxy's "Setting scanner mode" line already narrates this at D.
|
||||
ESP_LOGV(TAG, "Scan mode %s", active ? "active" : "passive");
|
||||
// Restart the controller scan only: the scan logically continues, so no
|
||||
// on_scan_end and no period reset. An idle scanner applies it on next start.
|
||||
// Apply to a running scan by restarting the CONTROLLER scan with the new
|
||||
// mode, bypassing the tracker's stop/start bookkeeping: no on_scan_end (the
|
||||
// scan logically continues, only the request mode changes), no period reset.
|
||||
// An idle scanner picks the mode up on its next start.
|
||||
if (this->scan_running_) {
|
||||
this->parent_->scan_stop();
|
||||
if (!this->controller_scan_start_()) {
|
||||
@@ -222,34 +184,20 @@ bool RP2BLETracker::request_scan_mode(bool active) {
|
||||
}
|
||||
|
||||
void RP2BLETracker::stop_scan() {
|
||||
// Cancel a start latched before setup(); without this an on_boot
|
||||
// start_scan/stop_scan pair would still start at the first loop().
|
||||
this->pending_start_ = false;
|
||||
this->scan_continuous_ = false;
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// A user stop during the OTA is the latest intent; the pause's own stop
|
||||
// (ota_pausing_) is exempt - it armed that state.
|
||||
if (this->ota_in_progress_ && !this->ota_pausing_) {
|
||||
this->scan_pending_before_ota_ = false;
|
||||
this->scan_continuous_before_ota_ = false;
|
||||
}
|
||||
#endif
|
||||
this->stop_scan_();
|
||||
// stop_scan_() early-returns when idle, so park here too - once set up, and
|
||||
// re-checked: its synchronous on_scan_end may have restarted the scan.
|
||||
if (this->is_ready() && !this->scan_running_ && !this->pending_start_) {
|
||||
this->disable_loop();
|
||||
}
|
||||
// stop_scan_() early-returns when no scan is running, so disable the loop
|
||||
// here too: a scan that never came up (stack still powering on at OTA start)
|
||||
// must not keep attempting scan_start() from the loop's retry branch.
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
// Stamp-and-start for every controller scan attempt: the stamp keeps the
|
||||
// SCAN_START_RETRY_MS floor covering all callers, not only loop()'s retry.
|
||||
bool RP2BLETracker::controller_scan_start_() {
|
||||
this->last_scan_start_attempt_ = App.get_loop_component_start_time();
|
||||
const bool ok = this->parent_->scan_start(static_cast<uint16_t>(this->scan_interval_),
|
||||
static_cast<uint16_t>(this->scan_window_), this->scan_active_);
|
||||
this->last_start_failed_ = !ok;
|
||||
return ok;
|
||||
return this->parent_->scan_start(static_cast<uint16_t>(this->scan_interval_),
|
||||
static_cast<uint16_t>(this->scan_window_), this->scan_active_);
|
||||
}
|
||||
|
||||
void RP2BLETracker::start_scan_() {
|
||||
@@ -260,15 +208,19 @@ void RP2BLETracker::start_scan_() {
|
||||
return;
|
||||
|
||||
this->scan_running_ = true;
|
||||
// Symmetric with stop_scan_()'s stop log; asymmetry would read as the
|
||||
// scanner failing to come back.
|
||||
// Log every explicit start at DEBUG — stop_scan_() logs every stop at DEBUG, and
|
||||
// in non-continuous mode each period is an explicit start, so asymmetric logging
|
||||
// would read as the scanner failing to come back up.
|
||||
ESP_LOGD(TAG, "Scan started (%s, window=%.0fms, interval=%.0fms)",
|
||||
this->scan_active_ ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("passive"),
|
||||
this->scan_window_ * BLE_SCAN_UNIT_MS, this->scan_interval_ * BLE_SCAN_UNIT_MS);
|
||||
// Anchor the period to the scan, not to boot, so a restart after a long gap
|
||||
// does not fire on_scan_end immediately. Same clock as loop()'s now.
|
||||
// Re-anchor the scan period to every successful start — first start (so the
|
||||
// period counts from the scan, not from boot) and every restart after a stop (so
|
||||
// resuming after longer than scan_duration, e.g. a failed OTA restoring continuous
|
||||
// mode 10 minutes later, does not fire on_scan_end before an advertisement can
|
||||
// arrive). Same clock as loop()'s `now`: a fresh millis() here would be ahead of
|
||||
// the cached loop time and make the same-iteration period check underflow.
|
||||
this->scan_period_start_ = App.get_loop_component_start_time();
|
||||
this->scan_start_time_ = this->scan_period_start_;
|
||||
}
|
||||
|
||||
void RP2BLETracker::stop_scan_() {
|
||||
@@ -280,9 +232,7 @@ void RP2BLETracker::stop_scan_() {
|
||||
this->fire_scan_end_();
|
||||
// Reset the period clock so on_scan_end does not double-fire; same clock as loop().
|
||||
this->scan_period_start_ = App.get_loop_component_start_time();
|
||||
// on_scan_end runs synchronously and may restart the scan; re-check before
|
||||
// parking or that scan runs untimed.
|
||||
if (!this->scan_continuous_ && !this->scan_running_ && !this->pending_start_) {
|
||||
if (!this->scan_continuous_) {
|
||||
// Nothing left to time; start_scan() re-enables the loop.
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
@@ -44,20 +44,11 @@ class RP2BLETracker : public Component,
|
||||
void set_scan_duration(uint32_t scan_duration) { this->scan_duration_ = scan_duration; }
|
||||
void set_scan_active(bool scan_active) { this->scan_active_ = scan_active; }
|
||||
void set_scan_continuous(bool scan_continuous) { this->scan_continuous_ = scan_continuous; }
|
||||
void set_configured_continuous(bool scan_continuous) {
|
||||
this->configured_continuous_ = scan_continuous;
|
||||
this->scan_continuous_ = scan_continuous;
|
||||
}
|
||||
bool scan_continuous() const { return this->scan_continuous_; }
|
||||
bool configured_continuous() const { return this->configured_continuous_; }
|
||||
|
||||
// ---- Public scan control ----
|
||||
// Mirrors esp32_ble_tracker: set_scan_continuous() + start_scan() / stop_scan().
|
||||
void start_scan();
|
||||
void stop_scan();
|
||||
// Re-anchors the one-shot duration clock only (bk72xx parity); no-op while
|
||||
// idle. Policy lives in the action.
|
||||
void restart_scan_duration();
|
||||
|
||||
// ---- ble_device_base::BLEHub contract ----
|
||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener) {
|
||||
@@ -67,8 +58,10 @@ class RP2BLETracker : public Component,
|
||||
this->dispatcher_.set_raw_advertisement_callback(callback);
|
||||
}
|
||||
static constexpr ble_device_base::HubCapabilities get_capabilities() {
|
||||
// Scan responses arrive separately and are merged before delivery
|
||||
// (Bluedroid semantics). GATT needs the BTstack connection backend.
|
||||
// BTstack delivers scan responses as separate advertisement reports; this
|
||||
// tracker merges the pair before delivery (shared ScanResponseMerger,
|
||||
// Bluedroid semantics). GATT is available when the BTstack connection
|
||||
// backend is compiled in (bluetooth_proxy active).
|
||||
#ifdef USE_BLE_GATT_CLIENT
|
||||
constexpr bool has_gatt = true;
|
||||
#else
|
||||
@@ -84,7 +77,8 @@ class RP2BLETracker : public Component,
|
||||
bool request_scan_mode(bool active);
|
||||
|
||||
// ---- rp2040_ble::BLEScanListener ----
|
||||
// Delivered on the main loop; the controller's queue did the IRQ handoff.
|
||||
// Delivered by the controller's loop() on the ESPHome main loop — the
|
||||
// IRQ → main-loop handoff already happened in the controller's queue.
|
||||
void on_scan_report(const rp2040_ble::BLEScanReport &report) override;
|
||||
|
||||
protected:
|
||||
@@ -99,29 +93,20 @@ class RP2BLETracker : public Component,
|
||||
uint32_t scan_window_{48}; // 48 × 0.625 ms = 30 ms (30/100 = 30 %)
|
||||
uint32_t scan_duration_{300000};
|
||||
uint32_t last_scan_start_attempt_{0}; // loop time of last start_scan_() attempt; rate-limits retries
|
||||
uint32_t scan_period_start_{0}; // continuous-mode on_scan_end period clock
|
||||
uint32_t scan_start_time_{0}; // one-shot duration clock (bk72xx parity: kept separate from the period)
|
||||
// Bit-packed (C++20 default member initializers on bit-fields);
|
||||
// scan_continuous_ stays a plain bool because the merger binds its address.
|
||||
bool scan_running_ : 1 {false};
|
||||
bool pending_start_ : 1 {false}; // start_scan() latched before setup() or while the stack is
|
||||
// not ACTIVE; loop() applies it once it is
|
||||
bool last_start_failed_ : 1 {false}; // last controller start failed; gates the public start_scan() floor
|
||||
bool scan_active_ : 1 {true};
|
||||
bool configured_continuous_ : 1 {true}; // YAML scan_parameters.continuous; runtime stop_scan() must not lose it
|
||||
uint32_t scan_period_start_{0}; // loop time at start of current scan period; rate-limits on_scan_end()
|
||||
bool scan_running_{false};
|
||||
bool scan_active_{true};
|
||||
bool scan_continuous_{true};
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Resume intent for a failed/aborted OTA: seeded at OTA start, overwritten
|
||||
// by a start/stop during the download, except from the pause's own stop.
|
||||
bool scan_continuous_before_ota_ : 1 {false}; // resume continuous
|
||||
bool scan_pending_before_ota_ : 1 {false}; // resume a one-shot scan
|
||||
bool ota_in_progress_ : 1 {false}; // OTA holds the radio; start_scan() defers to the resume path
|
||||
bool ota_pausing_ : 1 {false}; // inside the OTA's own stop_scan(); its latch clear is skipped
|
||||
bool scan_continuous_before_ota_{false}; // continuous mode saved at OTA start, restored on OTA failure
|
||||
bool scan_pending_before_ota_{false}; // one-shot scan in flight at OTA start, resumed on OTA failure
|
||||
#endif
|
||||
|
||||
// Shared merge + dispatch (ble_device_base), all on the main loop.
|
||||
// stash_adv() uses the parent's cached loop time and sweep() this one's -
|
||||
// same App.loop() pass, so the merger delta stays non-negative.
|
||||
// Shared adv + scan-response merge and frame dispatch (ble_device_base).
|
||||
// All calls run on the main loop. Merger clock: stash_adv() reads the
|
||||
// PARENT's cached loop time (on_scan_report runs inside rp2040_ble's queue
|
||||
// drain), sweep() this component's — same App.loop() pass, so the delta
|
||||
// stays non-negative and the 300 ms timeout holds.
|
||||
ble_device_base::ScanResponseMerger merger_;
|
||||
ble_device_base::AdvDispatcher dispatcher_;
|
||||
};
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "image_format.h"
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
struct MimeLookup {
|
||||
const char *mime_type;
|
||||
ImageFormat format;
|
||||
};
|
||||
|
||||
// The first entry per format is its canonical MIME type; the rest are aliases
|
||||
// seen from real servers (older IIS, CDNs, S3)
|
||||
static constexpr MimeLookup MIME_LOOKUP_TABLE[] = {
|
||||
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||
{"image/bmp", ImageFormat::BMP}, {"image/x-ms-bmp", ImageFormat::BMP}, {"image/x-bmp", ImageFormat::BMP},
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
{"image/jpeg", ImageFormat::JPEG}, {"image/jpg", ImageFormat::JPEG},
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||
{"image/png", ImageFormat::PNG}, {"image/x-png", ImageFormat::PNG},
|
||||
#endif
|
||||
};
|
||||
|
||||
const char *get_mime_type_for_format(ImageFormat format) {
|
||||
for (const auto &entry : MIME_LOOKUP_TABLE) {
|
||||
if (entry.format == format) {
|
||||
return entry.mime_type;
|
||||
}
|
||||
}
|
||||
return "image/*"; // AUTO or compiled-out format
|
||||
}
|
||||
|
||||
std::optional<ImageFormat> get_format_for_mime_type(const char *mime_type) {
|
||||
for (const auto &entry : MIME_LOOKUP_TABLE) {
|
||||
if (str_contains_ignore_case(mime_type, entry.mime_type)) {
|
||||
return entry.format;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/**
|
||||
@@ -19,9 +17,4 @@ enum ImageFormat {
|
||||
BMP,
|
||||
};
|
||||
|
||||
/// Canonical MIME type for a format; "image/*" for AUTO/unknown
|
||||
const char *get_mime_type_for_format(ImageFormat format);
|
||||
/// Case-insensitive substring match of known media types; nullopt if none found
|
||||
std::optional<ImageFormat> get_format_for_mime_type(const char *mime_type);
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "runtime_image.h"
|
||||
#include "image_decoder.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::sensor {
|
||||
|
||||
static const char *const TAG = "sensor.automation";
|
||||
|
||||
} // namespace esphome::sensor
|
||||
@@ -89,12 +89,12 @@ void SerialProxy::dump_config() {
|
||||
this->dtr_pin_ != nullptr ? "configured" : "not configured");
|
||||
}
|
||||
|
||||
SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control,
|
||||
uint8_t parity, uint8_t stop_bits, uint8_t data_size) {
|
||||
void SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity,
|
||||
uint8_t stop_bits, uint8_t data_size) {
|
||||
#ifdef USE_API
|
||||
if (this->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
ESP_LOGD(TAG,
|
||||
@@ -105,29 +105,25 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
auto *uart_comp = this->parent_;
|
||||
if (uart_comp == nullptr) {
|
||||
ESP_LOGE(TAG, "UART component not available");
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate all parameters before applying any (values come from a remote client)
|
||||
if (baudrate == 0) {
|
||||
ESP_LOGW(TAG, "Invalid baud rate: 0");
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
|
||||
return;
|
||||
}
|
||||
if (stop_bits < 1 || stop_bits > 2) {
|
||||
ESP_LOGW(TAG, "Invalid stop bits: %u (must be 1 or 2)", stop_bits);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
|
||||
return;
|
||||
}
|
||||
if (data_size < 5 || data_size > 8) {
|
||||
ESP_LOGW(TAG, "Invalid data bits: %u (must be 5-8)", data_size);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
|
||||
return;
|
||||
}
|
||||
if (parity > 2) {
|
||||
ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
|
||||
}
|
||||
if (flow_control) {
|
||||
ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported");
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply validated parameters
|
||||
@@ -147,7 +143,10 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
uart_comp->load_settings(true);
|
||||
#endif
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
|
||||
if (flow_control) {
|
||||
ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported");
|
||||
}
|
||||
}
|
||||
|
||||
void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {
|
||||
@@ -164,20 +163,13 @@ void SerialProxy::write_from_client(api::APIConnection *api_connection, const ui
|
||||
this->write_array(data, len);
|
||||
}
|
||||
|
||||
SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
|
||||
void SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
|
||||
#ifdef USE_API
|
||||
if (this->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring modem pin request from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Asserting a pin that is not configured must fail so the client learns the signal never
|
||||
// reached the wire; deasserting an absent pin is harmless and stays allowed. Clients can
|
||||
// avoid this by masking against SerialProxyInfo.configured_line_states.
|
||||
if ((line_states & ~this->get_configured_modem_pins()) != 0) {
|
||||
ESP_LOGW(TAG, "Requested modem pin not configured on serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
|
||||
}
|
||||
const bool rts = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_RTS) != 0;
|
||||
const bool dtr = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_DTR) != 0;
|
||||
ESP_LOGV(TAG, "Setting modem pins [%" PRIu32 "]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr));
|
||||
@@ -190,7 +182,6 @@ SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection
|
||||
this->dtr_state_ = dtr;
|
||||
this->dtr_pin_->digital_write(dtr);
|
||||
}
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
|
||||
uint32_t SerialProxy::get_modem_pins() const {
|
||||
@@ -198,26 +189,9 @@ uint32_t SerialProxy::get_modem_pins() const {
|
||||
(this->dtr_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u);
|
||||
}
|
||||
|
||||
SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
|
||||
#ifdef USE_API
|
||||
// Flushing stalls the port, so it gets the same ownership check as writes
|
||||
if (this->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring flush from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
}
|
||||
#endif
|
||||
uart::UARTFlushResult SerialProxy::flush_port() {
|
||||
ESP_LOGV(TAG, "Flushing serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
switch (this->flush()) {
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS:
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS:
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_ASSUMED_SUCCESS;
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT:
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_TIMEOUT;
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED:
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_ERROR;
|
||||
}
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_ERROR; // Unreachable; all enum values handled above
|
||||
return this->flush();
|
||||
}
|
||||
|
||||
#ifdef USE_API
|
||||
@@ -226,13 +200,12 @@ bool SerialProxy::port_claimed_by_other_(api::APIConnection *api_connection) con
|
||||
this->api_connection_->is_connection_setup();
|
||||
}
|
||||
|
||||
SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_connection,
|
||||
api::enums::SerialProxyRequestType type) {
|
||||
void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type) {
|
||||
switch (type) {
|
||||
case api::enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
if (this->api_connection_ == api_connection) {
|
||||
ESP_LOGV(TAG, "API connection is already subscribed to serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
return;
|
||||
}
|
||||
if (this->api_connection_ != nullptr) {
|
||||
// A living subscriber keeps exclusive access. Its connection may be dead without
|
||||
@@ -240,27 +213,26 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
|
||||
// in that case let the new client take over instead of locking it out.
|
||||
if (this->api_connection_->is_connection_setup()) {
|
||||
ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
|
||||
}
|
||||
this->api_connection_ = api_connection;
|
||||
this->enable_loop();
|
||||
ESP_LOGV(TAG, "API connection subscribed to serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
break;
|
||||
case api::enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
// Unsubscribe is idempotent: not being subscribed is not an error
|
||||
if (this->api_connection_ != api_connection) {
|
||||
ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
return;
|
||||
}
|
||||
this->api_connection_ = nullptr;
|
||||
this->disable_loop();
|
||||
ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast<uint32_t>(type));
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -38,17 +38,6 @@ enum SerialProxyLineStateFlag : uint32_t {
|
||||
SERIAL_PROXY_LINE_STATE_FLAG_DTR = 1 << 1, ///< DTR (Data Terminal Ready)
|
||||
};
|
||||
|
||||
/// Result of a client-initiated operation; mapped to api::enums::SerialProxyStatus by the API layer
|
||||
enum class SerialProxyResult : uint8_t {
|
||||
SERIAL_PROXY_RESULT_OK, ///< Operation completed or request accepted
|
||||
SERIAL_PROXY_RESULT_ASSUMED_SUCCESS, ///< Platform cannot confirm TX drain; success assumed
|
||||
SERIAL_PROXY_RESULT_PORT_IN_USE, ///< Denied: another live client holds the port
|
||||
SERIAL_PROXY_RESULT_INVALID_ARGUMENT, ///< A parameter value is out of range
|
||||
SERIAL_PROXY_RESULT_ERROR, ///< Driver or hardware error
|
||||
SERIAL_PROXY_RESULT_TIMEOUT, ///< Timed out before TX completed
|
||||
SERIAL_PROXY_RESULT_NOT_SUPPORTED, ///< Requested feature is not available on this instance
|
||||
};
|
||||
|
||||
/// Maximum bytes to read from UART in a single loop iteration
|
||||
inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256;
|
||||
|
||||
@@ -84,14 +73,14 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
/// @param parity Parity setting (0=none, 1=even, 2=odd)
|
||||
/// @param stop_bits Number of stop bits (1 or 2)
|
||||
/// @param data_size Number of data bits (5-8)
|
||||
SerialProxyResult configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity,
|
||||
uint8_t stop_bits, uint8_t data_size);
|
||||
void configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity,
|
||||
uint8_t stop_bits, uint8_t data_size);
|
||||
|
||||
/// Get the currently subscribed API connection (nullptr if none)
|
||||
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
||||
|
||||
/// Handle a subscribe/unsubscribe request from an API client
|
||||
SerialProxyResult serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type);
|
||||
void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type);
|
||||
|
||||
/// Write data received from an API client to the serial device
|
||||
/// @param api_connection The API connection sending the data
|
||||
@@ -100,20 +89,13 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len);
|
||||
|
||||
/// Set modem pin states from a bitmask of SerialProxyLineStateFlag values
|
||||
SerialProxyResult set_modem_pins(api::APIConnection *api_connection, uint32_t line_states);
|
||||
void set_modem_pins(api::APIConnection *api_connection, uint32_t line_states);
|
||||
|
||||
/// Get current modem pin states as a bitmask of SerialProxyLineStateFlag values
|
||||
uint32_t get_modem_pins() const;
|
||||
|
||||
/// Get the modem pins this instance can drive as a bitmask of SerialProxyLineStateFlag values
|
||||
uint32_t get_configured_modem_pins() const {
|
||||
return (this->rts_pin_ != nullptr ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) |
|
||||
(this->dtr_pin_ != nullptr ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u);
|
||||
}
|
||||
|
||||
/// Flush the serial port (block until all TX data is sent)
|
||||
/// @param api_connection The API connection requesting the flush
|
||||
SerialProxyResult flush_port(api::APIConnection *api_connection);
|
||||
uart::UARTFlushResult flush_port();
|
||||
|
||||
/// Set the RTS GPIO pin (from YAML configuration)
|
||||
void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; }
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
CODEOWNERS = ["@NoQuarrel"]
|
||||
@@ -1,79 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensirion_common, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FORMALDEHYDE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_GAS,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_FLASK_OUTLINE,
|
||||
ICON_THERMOMETER,
|
||||
ICON_WATER_PERCENT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
CONF_WAIT_FOR_READY = "wait_for_ready"
|
||||
|
||||
sfa40_ns = cg.esphome_ns.namespace("sfa40")
|
||||
SFA40Component = sfa40_ns.class_(
|
||||
"SFA40Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(SFA40Component),
|
||||
cv.Optional(CONF_WAIT_FOR_READY, default=True): cv.boolean,
|
||||
cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||
icon=ICON_FLASK_OUTLINE,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_GAS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_WATER_PERCENT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x5D))
|
||||
)
|
||||
|
||||
SENSOR_MAP = {
|
||||
CONF_FORMALDEHYDE: "set_formaldehyde_sensor",
|
||||
CONF_TEMPERATURE: "set_temperature_sensor",
|
||||
CONF_HUMIDITY: "set_humidity_sensor",
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_wait_for_ready(config[CONF_WAIT_FOR_READY]))
|
||||
|
||||
for key, func_name in SENSOR_MAP.items():
|
||||
if sensor_config := config.get(key):
|
||||
sens = await sensor.new_sensor(sensor_config)
|
||||
cg.add(getattr(var, func_name)(sens))
|
||||
@@ -1,159 +0,0 @@
|
||||
#include "sfa40.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::sfa40 {
|
||||
|
||||
static const char *const TAG = "sfa40";
|
||||
|
||||
// SFA40 Datasheet: https://sensirion.com/media/documents/5B06EDD9/69F84BD8/Sensirion_Datasheet_SFA40.pdf
|
||||
|
||||
static const uint16_t SFA40_CMD_START_MEASUREMENT = 0x00AC;
|
||||
static const uint16_t SFA40_CMD_STOP_MEASUREMENT = 0x50D2;
|
||||
static const uint16_t SFA40_CMD_READ_MEASURE_PROD = 0xC0EB;
|
||||
// B4 (engineering-sample) command codes. Commands from here: https://github.com/DFRobot/DFRobot_SFA40
|
||||
static const uint16_t SFA40_CMD_READ_MEASURE_B4 = 0xE06D;
|
||||
static const uint16_t SFA40_CMD_READ_ID_PROD = 0x02CE;
|
||||
static const uint16_t SFA40_CMD_READ_ID_B4 = 0x0559;
|
||||
static const uint8_t STATUS_NOT_READY = 0x01;
|
||||
static const uint8_t STATUS_OUT_OF_SPEC = 0x02;
|
||||
|
||||
static uint64_t raw_to_serial(const uint16_t *raw, size_t words) {
|
||||
uint64_t serial = 0;
|
||||
for (size_t i = 0; i < words; i++) {
|
||||
serial = (serial << 16) | raw[i];
|
||||
}
|
||||
return serial;
|
||||
}
|
||||
|
||||
static void raw_to_marking(const uint16_t *raw, size_t words, char *out, size_t out_len) {
|
||||
if (out_len < words * 2 + 1) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < words; i++) {
|
||||
out[i * 2] = static_cast<char>(raw[i] >> 8);
|
||||
out[i * 2 + 1] = static_cast<char>(raw[i] & 0xFF);
|
||||
}
|
||||
out[words * 2] = '\0';
|
||||
}
|
||||
|
||||
void SFA40Component::setup() {
|
||||
this->write_command(SFA40_CMD_STOP_MEASUREMENT);
|
||||
this->set_timeout(25, [this]() {
|
||||
if (!this->detect_protocol_()) {
|
||||
ESP_LOGE(TAG, "Failed to detect SFA40 protocol");
|
||||
this->error_code_ = PROTOCOL_DETECTION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (!this->write_command(SFA40_CMD_START_MEASUREMENT)) {
|
||||
ESP_LOGE(TAG, "Failed to start measurements");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->initialized_ = true;
|
||||
ESP_LOGD(TAG, "Measurement started");
|
||||
});
|
||||
}
|
||||
|
||||
bool SFA40Component::detect_protocol_() {
|
||||
uint16_t raw[5] = {};
|
||||
if (this->get_register(SFA40_CMD_READ_ID_PROD, raw, 3, 5)) {
|
||||
this->protocol_version_ = ProtocolVersion::PRODUCTION;
|
||||
this->serial_number_ = raw_to_serial(raw, 3);
|
||||
ESP_LOGD(TAG, "Detected production SFA40, serial number: %012" PRIX64, this->serial_number_);
|
||||
return true;
|
||||
}
|
||||
if (this->get_register(SFA40_CMD_READ_ID_B4, raw, 5, 5)) {
|
||||
this->protocol_version_ = ProtocolVersion::PROTOTYPE;
|
||||
raw_to_marking(raw, 5, this->device_marking_, sizeof(this->device_marking_));
|
||||
ESP_LOGD(TAG, "Detected engineering-sample SFA40, marking: '%s'", this->device_marking_);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SFA40Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "sfa40:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
switch (this->error_code_) {
|
||||
case PROTOCOL_DETECTION_FAILED:
|
||||
ESP_LOGW(TAG, "Protocol detection failed!");
|
||||
break;
|
||||
case MEASUREMENT_INIT_FAILED:
|
||||
ESP_LOGW(TAG, "Measurement initialization failed!");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown setup error!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
switch (this->protocol_version_) {
|
||||
case ProtocolVersion::PRODUCTION:
|
||||
ESP_LOGCONFIG(TAG, " Protocol: production\n Serial Number: %012" PRIX64, this->serial_number_);
|
||||
break;
|
||||
case ProtocolVersion::PROTOTYPE:
|
||||
ESP_LOGCONFIG(TAG, " Protocol: prototype (B4)\n Marking: '%s'", this->device_marking_);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGCONFIG(TAG, " Protocol: (detecting...)");
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Wait for ready: %s", YESNO(this->wait_for_ready_));
|
||||
LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
void SFA40Component::update() {
|
||||
if (!this->initialized_ || this->protocol_version_ == ProtocolVersion::UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint16_t read_cmd = (this->protocol_version_ == ProtocolVersion::PRODUCTION) ? SFA40_CMD_READ_MEASURE_PROD
|
||||
: SFA40_CMD_READ_MEASURE_B4;
|
||||
|
||||
if (!this->write_command(read_cmd)) {
|
||||
ESP_LOGW(TAG, "Error reading measurement");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(5, [this]() {
|
||||
uint16_t raw[4];
|
||||
if (!this->read_data(raw, 4)) {
|
||||
ESP_LOGW(TAG, "Error reading measurement data");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t status = raw[3] >> 8;
|
||||
const bool sensor_not_ready = (status & STATUS_NOT_READY) != 0;
|
||||
const bool sensor_out_of_spec = (status & STATUS_OUT_OF_SPEC) != 0;
|
||||
|
||||
if (this->formaldehyde_sensor_ != nullptr) {
|
||||
if (sensor_out_of_spec) {
|
||||
ESP_LOGW(TAG, "Skipping formaldehyde publish: sensor out of spec (status=0x%02X)", status);
|
||||
} else if (this->wait_for_ready_ && sensor_not_ready) {
|
||||
ESP_LOGD(TAG, "Skipping formaldehyde publish: sensor warming up");
|
||||
} else {
|
||||
this->formaldehyde_sensor_->publish_state(static_cast<float>(raw[0]) / 10.0f);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
this->humidity_sensor_->publish_state(clamp(125.0f * static_cast<float>(raw[1]) / 65535.0f - 6.0f, 0.0f, 100.0f));
|
||||
}
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
this->temperature_sensor_->publish_state(175.0f * (static_cast<float>(raw[2]) / 65535.0f) - 45.0f);
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace esphome::sfa40
|
||||
@@ -1,46 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
namespace esphome::sfa40 {
|
||||
|
||||
// SFA40 Datasheet: https://sensirion.com/media/documents/5B06EDD9/69F84BD8/Sensirion_Datasheet_SFA40.pdf
|
||||
|
||||
class SFA40Component final : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void set_formaldehyde_sensor(sensor::Sensor *formaldehyde) { this->formaldehyde_sensor_ = formaldehyde; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; }
|
||||
void set_wait_for_ready(bool wait_for_ready) { this->wait_for_ready_ = wait_for_ready; }
|
||||
|
||||
protected:
|
||||
enum ProtocolVersion : uint8_t {
|
||||
UNKNOWN = 0,
|
||||
PRODUCTION = 1,
|
||||
PROTOTYPE = 2,
|
||||
};
|
||||
enum ErrorCode : uint8_t {
|
||||
UNKNOWN_ERROR = 0,
|
||||
PROTOCOL_DETECTION_FAILED,
|
||||
MEASUREMENT_INIT_FAILED,
|
||||
};
|
||||
bool detect_protocol_();
|
||||
ProtocolVersion protocol_version_{UNKNOWN};
|
||||
ErrorCode error_code_{UNKNOWN_ERROR};
|
||||
char device_marking_[11]{};
|
||||
bool initialized_{false};
|
||||
bool wait_for_ready_{true};
|
||||
uint64_t serial_number_{0};
|
||||
|
||||
sensor::Sensor *formaldehyde_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::sfa40
|
||||
@@ -352,8 +352,6 @@ class SPIComponent final : public Component {
|
||||
this->using_hw_ = true;
|
||||
}
|
||||
|
||||
SPIInterface get_interface() const { return this->interface_; }
|
||||
|
||||
void set_interface_name(const char *name) { this->interface_name_ = name; }
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::switch_ {
|
||||
|
||||
static const char *const TAG = "switch.automation";
|
||||
|
||||
} // namespace esphome::switch_
|
||||
@@ -426,12 +426,7 @@ async def setup_time_core_(time_var, config):
|
||||
raise EsphomeError(f"Invalid timezone: {timezone}") from e
|
||||
_emit_parsed_timezone_fields(parsed)
|
||||
|
||||
on_time = config.get(CONF_ON_TIME, [])
|
||||
on_time_sync = config.get(CONF_ON_TIME_SYNC, [])
|
||||
if on_time or on_time_sync:
|
||||
cg.add_define("USE_TIME_TRIGGERS")
|
||||
|
||||
for conf in on_time:
|
||||
for conf in config.get(CONF_ON_TIME, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var)
|
||||
|
||||
seconds = conf.get(CONF_SECONDS, list(range(61)))
|
||||
@@ -450,7 +445,7 @@ async def setup_time_core_(time_var, config):
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in on_time_sync:
|
||||
for conf in config.get(CONF_ON_TIME_SYNC, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var)
|
||||
|
||||
await cg.register_component(trigger, conf)
|
||||
@@ -484,11 +479,7 @@ async def time_has_time_to_code(config, condition_id, template_arg, args):
|
||||
|
||||
|
||||
# posix_tz.cpp is fully #ifdef'd on USE_TIME_TIMEZONE, set only when a
|
||||
# timezone is configured or detected; automation.cpp holds the on_time and
|
||||
# on_time_sync triggers and is #ifdef'd on USE_TIME_TRIGGERS.
|
||||
# timezone is configured or detected.
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{
|
||||
"posix_tz.cpp": "USE_TIME_TIMEZONE",
|
||||
"automation.cpp": "USE_TIME_TRIGGERS",
|
||||
}
|
||||
{"posix_tz.cpp": "USE_TIME_TIMEZONE"}
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "automation.h"
|
||||
#ifdef USE_TIME_TRIGGERS
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -99,5 +98,3 @@ SyncTrigger::SyncTrigger(RealTimeClock *rtc) : rtc_(rtc) {
|
||||
}
|
||||
|
||||
} // namespace esphome::time
|
||||
|
||||
#endif // USE_TIME_TRIGGERS
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_TIME_TRIGGERS
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/time.h"
|
||||
@@ -52,5 +49,3 @@ class SyncTrigger final : public Trigger<>, public Component {
|
||||
RealTimeClock *rtc_;
|
||||
};
|
||||
} // namespace esphome::time
|
||||
|
||||
#endif // USE_TIME_TRIGGERS
|
||||
|
||||
@@ -393,16 +393,5 @@ void IRAM_ATTR IDFUARTComponent::uart_rx_isr_callback(uart_port_t uart_num, uart
|
||||
}
|
||||
#endif // USE_UART_WAKE_LOOP_ON_RX
|
||||
|
||||
void IDFUARTComponent::on_shutdown() {
|
||||
if (this->uart_num_ == UART_NUM_MAX || !uart_is_driver_installed(this->uart_num_))
|
||||
return;
|
||||
uart_wait_tx_done(this->uart_num_, pdMS_TO_TICKS(100));
|
||||
// Keep the peripheral quiet across a soft reset so ROM output does not reach the attached device (#15472)
|
||||
esp_err_t err = uart_driver_delete(this->uart_num_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::uart
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -52,11 +52,9 @@ class IDFUARTComponent final : public UARTComponent, public Component {
|
||||
void load_settings(bool dump_config) override;
|
||||
using UARTComponent::load_settings; // also bring in the no-arg overload for convenience
|
||||
|
||||
void on_shutdown() override;
|
||||
|
||||
protected:
|
||||
void check_logger_conflict() override;
|
||||
uart_port_t uart_num_{UART_NUM_MAX};
|
||||
uart_port_t uart_num_;
|
||||
uart_config_t get_config_();
|
||||
|
||||
bool has_peek_{false};
|
||||
|
||||
@@ -61,9 +61,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
if time_id_config := config.get(CONF_TIME_ID):
|
||||
time_id = await cg.get_variable(time_id_config)
|
||||
cg.add(var.set_time(time_id))
|
||||
cg.add_define("USE_UPTIME_TIMESTAMP")
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{"uptime_timestamp_sensor.cpp": "USE_UPTIME_TIMESTAMP"}
|
||||
{"uptime_timestamp_sensor.cpp": "USE_TIME"}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "uptime_timestamp_sensor.h"
|
||||
|
||||
#ifdef USE_UPTIME_TIMESTAMP
|
||||
#ifdef USE_TIME
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -34,4 +34,4 @@ void UptimeTimestampSensor::dump_config() {
|
||||
|
||||
} // namespace esphome::uptime
|
||||
|
||||
#endif // USE_UPTIME_TIMESTAMP
|
||||
#endif // USE_TIME
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_UPTIME_TIMESTAMP
|
||||
#ifdef USE_TIME
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
@@ -25,4 +25,4 @@ class UptimeTimestampSensor final : public sensor::Sensor, public Component {
|
||||
|
||||
} // namespace esphome::uptime
|
||||
|
||||
#endif // USE_UPTIME_TIMESTAMP
|
||||
#endif // USE_TIME
|
||||
|
||||
@@ -20,7 +20,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Re-enable esp-tls (excluded by default to save compile time);
|
||||
# web_server_idf.cpp includes <esp_tls_crypto.h> for digest auth
|
||||
include_builtin_idf_component("esp-tls")
|
||||
include_builtin_idf_component("esp_http_server")
|
||||
|
||||
|
||||
# multipart.cpp is fully #ifdef'd on USE_WEBSERVER_OTA (set by the
|
||||
|
||||
@@ -951,14 +951,6 @@ class WiFiComponent final : public Component {
|
||||
// On ESP8266, written from SDK system context (wifi_event_callback) —
|
||||
// uint8_t writes are atomic on Xtensa LX106 so no synchronization is needed.
|
||||
uint8_t sta_state_{0};
|
||||
#endif
|
||||
#ifdef USE_LIBRETINY
|
||||
// First attempt since STA-up (re-armed on every STA off->on); the
|
||||
// pre-attempt teardown is skipped then.
|
||||
bool lt_first_connect_attempt_{true};
|
||||
// A self-inflicted disconnect from that teardown is pending; it must not
|
||||
// consume an ignored-disconnect slot.
|
||||
bool lt_teardown_event_pending_{false};
|
||||
#endif
|
||||
RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY};
|
||||
RoamingState roaming_state_{RoamingState::IDLE};
|
||||
|
||||
@@ -115,8 +115,6 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
|
||||
|
||||
if (enable_sta && !current_sta) {
|
||||
ESP_LOGV(TAG, "Enabling STA");
|
||||
// Fresh STA stack: skip the pre-attempt teardown again.
|
||||
this->lt_first_connect_attempt_ = true;
|
||||
} else if (!enable_sta && current_sta) {
|
||||
ESP_LOGV(TAG, "Disabling STA");
|
||||
}
|
||||
@@ -204,21 +202,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
|
||||
// Tear down any live session so begin() re-fires its events; skipped on the
|
||||
// first attempt after STA-up (nothing to tear down, and BK7231N on the older
|
||||
// Beken SDK did not come back from it). The flag is per-attempt and armed
|
||||
// only for a live session: an idle disconnect may emit no event, and a stale
|
||||
// flag would swallow this attempt's first real failure.
|
||||
this->lt_teardown_event_pending_ = false;
|
||||
if (!this->lt_first_connect_attempt_) {
|
||||
const bool was_live = WiFi.status() == WL_CONNECTED;
|
||||
if (WiFi.disconnect()) {
|
||||
this->lt_teardown_event_pending_ = was_live;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Pre-connect teardown returned false");
|
||||
}
|
||||
String ssid = WiFi.SSID();
|
||||
if (ssid && strcmp(ssid.c_str(), ap.ssid_.c_str()) != 0) {
|
||||
WiFi.disconnect();
|
||||
}
|
||||
this->lt_first_connect_attempt_ = false;
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
||||
@@ -240,10 +227,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
ap.get_channel(), // 0 = auto
|
||||
ap.has_bssid() ? ap.get_bssid().data() : NULL);
|
||||
if (status != WL_CONNECTED) {
|
||||
ESP_LOGW(TAG, "WiFi.begin failed: %d", status);
|
||||
// Without this reset the state machine stays at CONNECTING and each retry
|
||||
// stalls for the full connect timeout (46 s).
|
||||
this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::ERROR_FAILED);
|
||||
ESP_LOGW(TAG, "esp_wifi_connect failed: %d", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -471,9 +455,6 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
|
||||
// Processed in queue order, so a teardown event still ahead of this
|
||||
// CONNECTED was already consumed; a leftover flag is stale.
|
||||
this->lt_teardown_event_pending_ = false;
|
||||
auto &it = event->data.sta_connected;
|
||||
char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(it.bssid, bssid_buf);
|
||||
@@ -501,14 +482,6 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: {
|
||||
auto &it = event->data.sta_disconnected;
|
||||
|
||||
// Consume the disconnect our own teardown queued, without spending an
|
||||
// ignore slot. Ungated on SSID and state: the flag is armed only for this
|
||||
// attempt's teardown of a live session.
|
||||
if (this->lt_teardown_event_pending_ && it.reason != WIFI_REASON_NO_AP_FOUND) {
|
||||
this->lt_teardown_event_pending_ = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// LibreTiny can send spurious disconnect events with empty ssid/bssid during connection.
|
||||
// These are typically "Association Leave" events that don't indicate actual failures:
|
||||
// [W][wifi_lt]: Disconnected ssid='' bssid=00:00:00:00:00:00 reason='Association Leave'
|
||||
|
||||
@@ -28,7 +28,6 @@ from esphome.platformio.library import (
|
||||
collect_filtered_files,
|
||||
convert_libraries,
|
||||
ensure_list,
|
||||
lex_build_flags,
|
||||
split_list_by_condition,
|
||||
)
|
||||
|
||||
@@ -39,13 +38,11 @@ ZEPHYR_FRAMEWORK = "zephyr"
|
||||
|
||||
|
||||
def _escape(p: PathType) -> str:
|
||||
# In CMakeLists.txt, backslashes and embedded quotes need escaping
|
||||
# (mirrors the ESP-IDF backend's escape_entry; the lex round-trip makes
|
||||
# a literal quote in a -D value reachable). Doubling backslashes --
|
||||
# rather than rewriting '\' -> '/' -- preserves content, so it's safe
|
||||
# for arbitrary build flags as well as Windows paths.
|
||||
escaped = str(p).replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
# In CMakeLists.txt, backslashes need to be escaped (mirrors the ESP-IDF
|
||||
# backend's escape_entry). Doubling -- rather than rewriting '\' -> '/' --
|
||||
# preserves content, so it's safe for arbitrary build flags (e.g. a -D value
|
||||
# containing a backslash) as well as Windows paths.
|
||||
return f'"{str(p)}"'.replace("\\", "\\\\")
|
||||
|
||||
|
||||
def generate_module_yml(component: ConvertedLibrary) -> str:
|
||||
@@ -83,11 +80,7 @@ def generate_cmakelists_txt(component: ConvertedLibrary) -> str:
|
||||
|
||||
build_include_dir = build.get("includeDir", DEFAULT_BUILD_INCLUDE_DIR)
|
||||
build_src_filter = ensure_list(build.get("srcFilter", DEFAULT_BUILD_SRC_FILTER))
|
||||
# The shared lexer re-glues spaced entries and drops bare/empty
|
||||
# arguments, same as the espidf emitter
|
||||
build_flags = lex_build_flags(
|
||||
build.get("flags", DEFAULT_BUILD_FLAGS), component.name
|
||||
)
|
||||
build_flags = ensure_list(build.get("flags", DEFAULT_BUILD_FLAGS))
|
||||
|
||||
src_files = collect_filtered_files(
|
||||
read_path / Path(build_src_dir), build_src_filter
|
||||
|
||||
@@ -9,7 +9,6 @@ SCALE = "scale"
|
||||
CONF_ATTRIBUTE_ID = "attribute_id"
|
||||
KEY_ZIGBEE_EP = "zigbee_ep"
|
||||
KEY_ZIGBEE_EP_NO_NUM = "zigbee_ep_no_num"
|
||||
KEY_ZIGBEE_FIRST_EP_CL = "zigbee_first_ep_cl"
|
||||
|
||||
DEVICE_ID = {
|
||||
"RANGE_EXTENDER": cg.RawExpression("EZB_ZHA_RANGE_EXTENDER_DEVICE_ID"),
|
||||
@@ -19,13 +18,11 @@ DEVICE_ID = {
|
||||
cluster_id = cg.esphome_ns.enum("ezb_zcl_cluster_id_e")
|
||||
CLUSTER_ID = {
|
||||
"BASIC": cluster_id.EZB_ZCL_CLUSTER_ID_BASIC,
|
||||
"TIME": cluster_id.EZB_ZCL_CLUSTER_ID_TIME,
|
||||
"BINARY_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_BINARY_INPUT,
|
||||
"ANALOG_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
|
||||
}
|
||||
CLUSTER_ROLE = {
|
||||
"SERVER": cg.RawExpression("EZB_ZCL_CLUSTER_SERVER"),
|
||||
"CLIENT": cg.RawExpression("EZB_ZCL_CLUSTER_CLIENT"),
|
||||
}
|
||||
attr_type = cg.esphome_ns.enum("ezb_zcl_attr_type_e")
|
||||
ATTR_TYPE = {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import consume_endpoint
|
||||
from ..const import zigbee_ns
|
||||
from ..const_esp32 import ROLE
|
||||
from ..const_zephyr import CONF_ZIGBEE_ID
|
||||
from ..zigbee_ep_esp32 import add_clusters_to_first_ep, get_first_ep_num
|
||||
from ..zigbee_zephyr import (
|
||||
ZigbeeClusterDesc,
|
||||
ZigbeeComponent,
|
||||
@@ -24,52 +22,26 @@ DEPENDENCIES = ["zigbee"]
|
||||
|
||||
ZigbeeTime = zigbee_ns.class_("ZigbeeTime", time_.RealTimeClock)
|
||||
|
||||
|
||||
def _validate_zigbee_time(config: ConfigType) -> ConfigType:
|
||||
if CORE.is_nrf52:
|
||||
return consume_endpoint(config)
|
||||
if CORE.is_esp32:
|
||||
cl = [
|
||||
{
|
||||
CONF_ID: "TIME",
|
||||
ROLE: "CLIENT",
|
||||
},
|
||||
{
|
||||
CONF_ID: "TIME",
|
||||
ROLE: "SERVER",
|
||||
},
|
||||
]
|
||||
add_clusters_to_first_ep(cl)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
time_.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ZigbeeTime),
|
||||
cv.GenerateID(CONF_ZIGBEE_ID): cv.use_id(ZigbeeComponent),
|
||||
cv.SplitDefault(
|
||||
CONF_UPDATE_INTERVAL,
|
||||
nrf52="1s",
|
||||
esp32="15min",
|
||||
): cv.update_interval, # override default from TIME_SCHEMA. Remove once nrf52 implementation is aligned.
|
||||
cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(
|
||||
ZigbeeComponent
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_validate_zigbee_time,
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("1s")),
|
||||
consume_endpoint,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if CORE.using_zephyr:
|
||||
CORE.add_job(_add_time_zephyr, config)
|
||||
if CORE.is_esp32:
|
||||
zb = await cg.get_variable(config[CONF_ZIGBEE_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID], zb, get_first_ep_num())
|
||||
await cg.register_component(var, config)
|
||||
await time_.register_time(var, config)
|
||||
CORE.add_job(_add_time, config)
|
||||
|
||||
|
||||
async def _add_time_zephyr(config: ConfigType) -> None:
|
||||
async def _add_time(config: ConfigType) -> None:
|
||||
slot_index = get_slot_index()
|
||||
|
||||
# Create unique names for this sensor's variables based on slot index
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
#include "zigbee_time_esp32.h"
|
||||
#if defined(USE_ZIGBEE) && defined(USE_ESP32) && defined(USE_TIME)
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::zigbee {
|
||||
|
||||
static const char *const TAG = "zigbee.time";
|
||||
|
||||
// This time standard is the number of
|
||||
// seconds since 0 hrs 0 mins 0 sec on 1st January 2000 UTC (Universal Coordinated Time).
|
||||
constexpr time_t EPOCH_2000 = 946684800;
|
||||
|
||||
static ZigbeeTime *global_time = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void ZigbeeTime::setup() {
|
||||
global_time = this;
|
||||
if (this->parent_->is_started()) {
|
||||
this->register_zb_time_();
|
||||
} else {
|
||||
this->parent_->add_on_start_callback([this]() { this->register_zb_time_(); });
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeTime::register_zb_time_() {
|
||||
ezb_zcl_time_interface_t time_interface = {
|
||||
.get_utc_time = esphome::zigbee::ZigbeeTime::get_utc_time,
|
||||
.set_utc_time = esphome::zigbee::ZigbeeTime::set_utc_time,
|
||||
};
|
||||
ezb_err_t ret;
|
||||
if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
|
||||
this->set_timeout("zb_time_register", 100, [this]() { this->register_zb_time_(); });
|
||||
return;
|
||||
}
|
||||
ret = ezb_zcl_time_server_interface_register(this->endpoint_, time_interface);
|
||||
esp_zigbee_lock_release();
|
||||
if (ret != EZB_ERR_NONE) {
|
||||
ESP_LOGW(TAG, "Setup failed: %d", ret);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->registered_ = true;
|
||||
this->parent_->add_on_join_callback([this](bool x) { this->update(); });
|
||||
if (this->parent_->is_joined()) {
|
||||
this->update();
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeTime::status_cb(ezb_err_t status) {
|
||||
if (status == EZB_ERR_NONE) {
|
||||
ESP_LOGV(TAG, "Time synchronization successful");
|
||||
} else if (status == EZB_ERR_TIMEOUT) {
|
||||
ESP_LOGW(TAG, "Time synchronization timed out");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Time synchronization failed with error: %d", status);
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeTime::update() {
|
||||
if (this->parent_->is_joined() && this->registered_) {
|
||||
if (esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
|
||||
ESP_LOGV(TAG, "Updating time sync from Zigbee network...");
|
||||
ezb_zcl_time_server_synchronize_time(this->endpoint_, 10, esphome::zigbee::ZigbeeTime::status_cb,
|
||||
EZB_ZCL_TIME_SERVER_RANK_MASTER);
|
||||
esp_zigbee_lock_release();
|
||||
this->retry_count_ = 0;
|
||||
} else {
|
||||
if (this->retry_count_ == 0) {
|
||||
ESP_LOGW(TAG, "Could not acquire Zigbee lock to synchronize time, will retry maximum 3 times");
|
||||
}
|
||||
if (this->retry_count_ < 3) {
|
||||
this->set_timeout("zb_time_sync", 100, [this]() { this->update(); });
|
||||
this->retry_count_++;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Could not acquire Zigbee lock to synchronize time");
|
||||
this->retry_count_ = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Not connected to Zigbee network, cannot synchronize time");
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ZigbeeTime::get_utc_time() {
|
||||
const time_t now = global_time->timestamp_now();
|
||||
if (now < EPOCH_2000) {
|
||||
return 0xFFFFFFFF; // ZCL invalid UTCTime
|
||||
}
|
||||
return (uint32_t) (now - EPOCH_2000);
|
||||
}
|
||||
|
||||
void ZigbeeTime::set_utc_time(uint32_t utc) {
|
||||
// prevent overflow
|
||||
if (utc <= (std::numeric_limits<uint32_t>::max() - EPOCH_2000)) {
|
||||
global_time->set_epoch_time(utc + EPOCH_2000);
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeTime::set_epoch_time(uint32_t utc) {
|
||||
// called from zigbee task, defer to main loop
|
||||
this->defer([this, utc]() {
|
||||
ESP_LOGV(TAG, "Setting device time to UTC: %u", static_cast<unsigned>(utc));
|
||||
this->synchronize_epoch_(utc);
|
||||
});
|
||||
App.wake_loop_threadsafe();
|
||||
}
|
||||
|
||||
void ZigbeeTime::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Zigbee Time\n"
|
||||
" Endpoint: %u",
|
||||
this->endpoint_);
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
} // namespace esphome::zigbee
|
||||
|
||||
#endif
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_ZIGBEE) && defined(USE_ESP32) && defined(USE_TIME)
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#include "../zigbee_esp32.h"
|
||||
|
||||
namespace esphome::zigbee {
|
||||
|
||||
class ZigbeeComponent;
|
||||
|
||||
class ZigbeeTime final : public time::RealTimeClock {
|
||||
public:
|
||||
ZigbeeTime(ZigbeeComponent *parent, uint8_t ep) : parent_(parent), endpoint_(ep) {}
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
void set_epoch_time(uint32_t utc);
|
||||
|
||||
protected:
|
||||
void register_zb_time_();
|
||||
static void set_utc_time(uint32_t utc);
|
||||
static uint32_t get_utc_time();
|
||||
static void status_cb(ezb_err_t status);
|
||||
|
||||
ZigbeeComponent *parent_;
|
||||
uint8_t endpoint_;
|
||||
uint8_t retry_count_{0};
|
||||
bool registered_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::zigbee
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user