Merge remote-tracking branch 'upstream/dev' into add_api_stats

This commit is contained in:
J. Nick Koston 2025-05-22 18:04:25 -05:00
commit 1c06137ae0
No known key found for this signature in database
179 changed files with 5882 additions and 893 deletions

37
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,37 @@
ARG BUILD_BASE_VERSION=2025.04.0
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
RUN git config --system --add safe.directory "*"
RUN apt update \
&& apt install -y \
protobuf-compiler
RUN pip install uv
RUN useradd esphome -m
USER esphome
ENV VIRTUAL_ENV=/home/esphome/.local/esphome-venv
RUN uv venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Override this set to true in the docker-base image
ENV UV_SYSTEM_PYTHON=false
WORKDIR /tmp
COPY requirements.txt ./
RUN uv pip install -r requirements.txt
COPY requirements_dev.txt requirements_test.txt ./
RUN uv pip install -r requirements_dev.txt -r requirements_test.txt
RUN \
platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000
COPY script/platformio_install_deps.py platformio.ini ./
RUN ./platformio_install_deps.py platformio.ini --libraries --platforms --tools
WORKDIR /workspaces

View File

@ -1,18 +1,17 @@
{ {
"name": "ESPHome Dev", "name": "ESPHome Dev",
"image": "ghcr.io/esphome/esphome-lint:dev", "context": "..",
"dockerFile": "Dockerfile",
"postCreateCommand": [ "postCreateCommand": [
"script/devcontainer-post-create" "script/devcontainer-post-create"
], ],
"containerEnv": { "features": {
"DEVCONTAINER": "1", "ghcr.io/devcontainers/features/github-cli:1": {}
"PIP_BREAK_SYSTEM_PACKAGES": "1",
"PIP_ROOT_USER_ACTION": "ignore"
}, },
"runArgs": [ "runArgs": [
"--privileged", "--privileged",
"-e", "-e",
"ESPHOME_DASHBOARD_USE_PING=1" "GIT_EDITOR=code --wait"
// uncomment and edit the path in order to pass though local USB serial to the conatiner // uncomment and edit the path in order to pass though local USB serial to the conatiner
// , "--device=/dev/ttyACM0" // , "--device=/dev/ttyACM0"
], ],

View File

@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest - name: Build and push to ghcr by digest
id: build-ghcr id: build-ghcr
uses: docker/build-push-action@v6.16.0 uses: docker/build-push-action@v6.17.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest - name: Build and push to dockerhub by digest
id: build-dockerhub id: build-dockerhub
uses: docker/build-push-action@v6.16.0 uses: docker/build-push-action@v6.17.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false

View File

@ -47,7 +47,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.9" python-version: "3.10"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0 uses: docker/setup-buildx-action@v3.10.0

View File

@ -20,8 +20,8 @@ permissions:
contents: read contents: read
env: env:
DEFAULT_PYTHON: "3.9" DEFAULT_PYTHON: "3.10"
PYUPGRADE_TARGET: "--py39-plus" PYUPGRADE_TARGET: "--py310-plus"
concurrency: concurrency:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@ -173,10 +173,10 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: python-version:
- "3.9"
- "3.10" - "3.10"
- "3.11" - "3.11"
- "3.12" - "3.12"
- "3.13"
os: os:
- ubuntu-latest - ubuntu-latest
- macOS-latest - macOS-latest
@ -185,18 +185,18 @@ jobs:
# Minimize CI resource usage # Minimize CI resource usage
# by only running the Python version # by only running the Python version
# version used for docker images on Windows and macOS # version used for docker images on Windows and macOS
- python-version: "3.13"
os: windows-latest
- python-version: "3.12" - python-version: "3.12"
os: windows-latest os: windows-latest
- python-version: "3.10" - python-version: "3.10"
os: windows-latest os: windows-latest
- python-version: "3.9" - python-version: "3.13"
os: windows-latest os: macOS-latest
- python-version: "3.12" - python-version: "3.12"
os: macOS-latest os: macOS-latest
- python-version: "3.10" - python-version: "3.10"
os: macOS-latest os: macOS-latest
- python-version: "3.9"
os: macOS-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: needs:
- common - common
@ -221,7 +221,7 @@ jobs:
. venv/bin/activate . venv/bin/activate
pytest -vv --cov-report=xml --tb=native tests pytest -vv --cov-report=xml --tb=native tests
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.2 uses: codecov/codecov-action@v5.4.3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -18,6 +18,7 @@ jobs:
outputs: outputs:
tag: ${{ steps.tag.outputs.tag }} tag: ${{ steps.tag.outputs.tag }}
branch_build: ${{ steps.tag.outputs.branch_build }} branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Get tag - name: Get tag
@ -27,6 +28,11 @@ jobs:
if [[ "${{ github.event_name }}" = "release" ]]; then if [[ "${{ github.event_name }}" = "release" ]]; then
TAG="${{ github.event.release.tag_name}}" TAG="${{ github.event.release.tag_name}}"
BRANCH_BUILD="false" BRANCH_BUILD="false"
if [[ "${{ github.event.release.prerelease }}" = "true" ]]; then
ENVIRONMENT="beta"
else
ENVIRONMENT="production"
fi
else else
TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p")
today="$(date --utc '+%Y%m%d')" today="$(date --utc '+%Y%m%d')"
@ -35,12 +41,15 @@ jobs:
if [[ "$BRANCH" != "dev" ]]; then if [[ "$BRANCH" != "dev" ]]; then
TAG="${TAG}-${BRANCH}" TAG="${TAG}-${BRANCH}"
BRANCH_BUILD="true" BRANCH_BUILD="true"
ENVIRONMENT=""
else else
BRANCH_BUILD="false" BRANCH_BUILD="false"
ENVIRONMENT="dev"
fi fi
fi fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT
echo "deploy_env=${ENVIRONMENT}" >> $GITHUB_OUTPUT
# yamllint enable rule:line-length # yamllint enable rule:line-length
deploy-pypi: deploy-pypi:
@ -87,7 +96,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.9" python-version: "3.10"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0 uses: docker/setup-buildx-action@v3.10.0
@ -233,9 +242,8 @@ jobs:
deploy-esphome-schema: deploy-esphome-schema:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false' if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs: [init]
- init environment: ${{ needs.init.outputs.deploy_env }}
- deploy-manifest
steps: steps:
- name: Trigger Workflow - name: Trigger Workflow
uses: actions/github-script@v7.0.1 uses: actions/github-script@v7.0.1

View File

@ -13,10 +13,10 @@ jobs:
if: github.repository == 'esphome/esphome' if: github.repository == 'esphome/esphome'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Checkout Home Assistant - name: Checkout Home Assistant
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
with: with:
repository: home-assistant/core repository: home-assistant/core
path: lib/home-assistant path: lib/home-assistant
@ -24,7 +24,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: 3.12 python-version: 3.13
- name: Install Home Assistant - name: Install Home Assistant
run: | run: |

1
.gitignore vendored
View File

@ -143,3 +143,4 @@ sdkconfig.*
/components /components
/managed_components /managed_components
api-docs/

View File

@ -4,7 +4,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.11.9 rev: v0.11.10
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@ -28,10 +28,10 @@ repos:
- --branch=release - --branch=release
- --branch=beta - --branch=beta
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.15.2 rev: v3.19.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py39-plus] args: [--py310-plus]
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1 rev: v1.37.1
hooks: hooks:

View File

@ -96,6 +96,7 @@ esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/chsc6x/* @kkosik20 esphome/components/chsc6x/* @kkosik20
esphome/components/climate/* @esphome/core esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet esphome/components/climate_ir/* @glmnet
esphome/components/cm1106/* @andrewjswan
esphome/components/color_temperature/* @jesserockz esphome/components/color_temperature/* @jesserockz
esphome/components/combination/* @Cat-Ion @kahrendt esphome/components/combination/* @Cat-Ion @kahrendt
esphome/components/const/* @esphome/core esphome/components/const/* @esphome/core
@ -478,6 +479,8 @@ esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter esphome/components/ultrasonic/* @OttoWinter
esphome/components/update/* @jesserockz esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_host/* @clydebarrow
esphome/components/usb_uart/* @clydebarrow
esphome/components/valve/* @esphome/core esphome/components/valve/* @esphome/core
esphome/components/vbus/* @ssieb esphome/components/vbus/* @ssieb
esphome/components/veml3235/* @kbx81 esphome/components/veml3235/* @kbx81

2877
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,9 @@ FROM base-source-${BUILD_TYPE} AS base
RUN git config --system --add safe.directory "*" RUN git config --system --add safe.directory "*"
RUN pip install uv==0.6.14 ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14
COPY requirements.txt / COPY requirements.txt /

View File

@ -11,6 +11,7 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/version.h" #include "esphome/core/version.h"
#include "esphome/core/application.h"
#ifdef USE_DEEP_SLEEP #ifdef USE_DEEP_SLEEP
#include "esphome/components/deep_sleep/deep_sleep_component.h" #include "esphome/components/deep_sleep/deep_sleep_component.h"
@ -81,7 +82,11 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
#endif #endif
} }
void APIConnection::start() { void APIConnection::start() {
this->last_traffic_ = millis(); this->last_traffic_ = App.get_loop_component_start_time();
// Set next_ping_retry_ to prevent immediate ping
// This ensures the first ping happens after the keepalive period
this->next_ping_retry_ = this->last_traffic_ + KEEPALIVE_TIMEOUT_MS;
APIError err = this->helper_->init(); APIError err = this->helper_->init();
if (err != APIError::OK) { if (err != APIError::OK) {
@ -167,7 +172,7 @@ void APIConnection::loop() {
} }
return; return;
} else { } else {
this->last_traffic_ = now; this->last_traffic_ = App.get_loop_component_start_time();
// Section: Process Message // Section: Process Message
start_time = millis(); start_time = millis();
@ -198,17 +203,16 @@ void APIConnection::loop() {
// Section: Keepalive // Section: Keepalive
start_time = millis(); start_time = millis();
static uint32_t keepalive = 60000;
static uint8_t max_ping_retries = 60; static uint8_t max_ping_retries = 60;
static uint16_t ping_retry_interval = 1000; static uint16_t ping_retry_interval = 1000;
const uint32_t now = App.get_loop_component_start_time();
if (this->sent_ping_) { if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > (keepalive * 5) / 2) { if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str()); ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str());
} }
} else if (now - this->last_traffic_ > keepalive && now > this->next_ping_retry_) { } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) {
ESP_LOGVV(TAG, "Sending keepalive PING..."); ESP_LOGVV(TAG, "Sending keepalive PING...");
this->sent_ping_ = this->send_ping_request(PingRequest()); this->sent_ping_ = this->send_ping_request(PingRequest());
if (!this->sent_ping_) { if (!this->sent_ping_) {
@ -1716,10 +1720,9 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
} }
uint32_t write_start = millis(); uint32_t write_start = millis();
APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size()); APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
uint32_t write_duration = millis() - write_start; uint32_t write_duration = millis() - write_start;
this->section_stats_["write_packet"].record_time(write_duration); this->section_stats_["write_packet"].record_time(write_duration);
if (err == APIError::WOULD_BLOCK) if (err == APIError::WOULD_BLOCK)
return false; return false;
if (err != APIError::OK) { if (err != APIError::OK) {

View File

@ -19,6 +19,9 @@
namespace esphome { namespace esphome {
namespace api { namespace api {
// Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
using send_message_t = bool (APIConnection::*)(void *); using send_message_t = bool (APIConnection::*)(void *);
/* /*
@ -468,7 +471,14 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
// FIXME: ensure no recursive writes can happen // FIXME: ensure no recursive writes can happen
this->proto_write_buffer_.clear(); this->proto_write_buffer_.clear();
this->proto_write_buffer_.reserve(reserve_size); // Get header padding size - used for both reserve and insert
uint8_t header_padding = this->helper_->frame_header_padding();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
this->proto_write_buffer_.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
// Insert header padding bytes so message encoding starts at the correct position
this->proto_write_buffer_.insert(this->proto_write_buffer_.begin(), header_padding, 0);
return {&this->proto_write_buffer_}; return {&this->proto_write_buffer_};
} }
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space);

View File

@ -7,20 +7,13 @@
#include "proto.h" #include "proto.h"
#include "api_pb2_size.h" #include "api_pb2_size.h"
#include <cstring> #include <cstring>
#include <cinttypes>
namespace esphome { namespace esphome {
namespace api { namespace api {
static const char *const TAG = "api.socket"; static const char *const TAG = "api.socket";
/// Is the given return value (from write syscalls) a wouldblock error?
bool is_would_block(ssize_t ret) {
if (ret == -1) {
return errno == EWOULDBLOCK || errno == EAGAIN;
}
return ret == 0;
}
const char *api_error_to_str(APIError err) { const char *api_error_to_str(APIError err) {
// not using switch to ensure compiler doesn't try to build a big table out of it // not using switch to ensure compiler doesn't try to build a big table out of it
if (err == APIError::OK) { if (err == APIError::OK) {
@ -73,92 +66,154 @@ const char *api_error_to_str(APIError err) {
return "UNKNOWN"; return "UNKNOWN";
} }
// Common implementation for writing raw data to socket // Helper method to buffer data from IOVs
template<typename StateEnum> void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, SendBuffer buffer;
std::vector<uint8_t> &tx_buf, const std::string &info, StateEnum &state, buffer.data.reserve(total_write_len);
StateEnum failed_state) { for (int i = 0; i < iovcnt; i++) {
// This method writes data to socket or buffers it const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base);
buffer.data.insert(buffer.data.end(), data, data + iov[i].iov_len);
}
this->tx_buf_.push_back(std::move(buffer));
}
// This method writes data to socket or buffers it
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
// Returns APIError::OK if successful (or would block, but data has been buffered) // Returns APIError::OK if successful (or would block, but data has been buffered)
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to failed_state // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
if (iovcnt == 0) if (iovcnt == 0)
return APIError::OK; // Nothing to do, success return APIError::OK; // Nothing to do, success
size_t total_write_len = 0; uint16_t total_write_len = 0;
for (int i = 0; i < iovcnt; i++) { for (int i = 0; i < iovcnt; i++) {
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s", ESP_LOGVV(TAG, "Sending raw: %s",
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str()); format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
#endif #endif
total_write_len += iov[i].iov_len; total_write_len += static_cast<uint16_t>(iov[i].iov_len);
} }
if (!tx_buf.empty()) { // Try to send any existing buffered data first if there is any
// try to empty tx_buf first if (!this->tx_buf_.empty()) {
while (!tx_buf.empty()) { APIError send_result = try_send_tx_buf_();
ssize_t sent = socket->write(tx_buf.data(), tx_buf.size()); // If real error occurred (not just WOULD_BLOCK), return it
if (is_would_block(sent)) { if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
break; return send_result;
} else if (sent == -1) { }
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
state = failed_state; // If there is still data in the buffer, we can't send, buffer
return APIError::SOCKET_WRITE_FAILED; // Socket write failed // the new data and return
} if (!this->tx_buf_.empty()) {
// TODO: inefficient if multiple packets in txbuf this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
// replace with deque of buffers return APIError::OK; // Success, data buffered
tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent);
} }
} }
if (!tx_buf.empty()) { // Try to send directly if no buffered data
// tx buf not empty, can't write now because then stream would be inconsistent ssize_t sent = this->socket_->writev(iov, iovcnt);
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + total_write_len);
for (int i = 0; i < iovcnt; i++) {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
}
return APIError::OK; // Success, data buffered
}
ssize_t sent = socket->writev(iov, iovcnt); if (sent == -1) {
if (is_would_block(sent)) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
// operation would block, add buffer to tx_buf // Socket would block, buffer the data
// Reserve space upfront to avoid multiple reallocations this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
tx_buf.reserve(tx_buf.size() + total_write_len); return APIError::OK; // Success, data buffered
for (int i = 0; i < iovcnt; i++) {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
} }
return APIError::OK; // Success, data buffered // Socket error
} else if (sent == -1) { ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
// an error occurred this->state_ = State::FAILED;
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
state = failed_state;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed return APIError::SOCKET_WRITE_FAILED; // Socket write failed
} else if ((size_t) sent != total_write_len) { } else if (static_cast<uint16_t>(sent) < total_write_len) {
// partially sent, add end to tx_buf // Partially sent, buffer the remaining data
size_t remaining = total_write_len - sent; SendBuffer buffer;
// Reserve space upfront to avoid multiple reallocations uint16_t to_consume = static_cast<uint16_t>(sent);
tx_buf.reserve(tx_buf.size() + remaining); uint16_t remaining = total_write_len - static_cast<uint16_t>(sent);
buffer.data.reserve(remaining);
size_t to_consume = sent;
for (int i = 0; i < iovcnt; i++) { for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) { if (to_consume >= iov[i].iov_len) {
to_consume -= iov[i].iov_len; // This segment was fully sent
to_consume -= static_cast<uint16_t>(iov[i].iov_len);
} else { } else {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume, // This segment was partially sent or not sent at all
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len); const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_consume;
buffer.data.insert(buffer.data.end(), data, data + len);
to_consume = 0; to_consume = 0;
} }
} }
return APIError::OK; // Success, data buffered
this->tx_buf_.push_back(std::move(buffer));
} }
return APIError::OK; // Success, all data sent
return APIError::OK; // Success, all data sent or buffered
} }
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) // Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method
APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
bool tx_buf_empty = false;
while (!tx_buf_empty) {
// Get the first buffer in the queue
SendBuffer &front_buffer = this->tx_buf_.front();
// Try to send the remaining data in this buffer
ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
if (sent == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
// Real socket error (not just would block)
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
}
// Socket would block, we'll try again later
return APIError::WOULD_BLOCK;
} else if (sent == 0) {
// Nothing sent but not an error
return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) {
// Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t
front_buffer.offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
} else {
// Buffer completely sent, remove it from the queue
this->tx_buf_.pop_front();
// Update empty status for the loop condition
tx_buf_empty = this->tx_buf_.empty();
// Continue loop to try sending the next buffer
}
}
return APIError::OK; // All buffers sent successfully
}
APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
ESP_LOGVV(TAG, "%s: Bad state for init %d", this->info_.c_str(), (int) state_);
return APIError::BAD_STATE;
}
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
ESP_LOGVV(TAG, "%s: Setting nonblocking failed with errno %d", this->info_.c_str(), errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
ESP_LOGVV(TAG, "%s: Setting nodelay failed with errno %d", this->info_.c_str(), errno);
return APIError::TCP_NODELAY_FAILED;
}
return APIError::OK;
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
// uncomment to log raw packets // uncomment to log raw packets
//#define HELPER_LOG_PACKETS //#define HELPER_LOG_PACKETS
@ -206,23 +261,9 @@ std::string noise_err_to_str(int err) {
/// Initialize the frame helper, returns OK if successful. /// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() { APIError APINoiseFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) { APIError err = init_common_();
HELPER_LOG("Bad state for init %d", (int) state_); if (err != APIError::OK) {
return APIError::BAD_STATE; return err;
}
int err = socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED;
} }
// init prologue // init prologue
@ -234,17 +275,16 @@ APIError APINoiseFrameHelper::init() {
/// Run through handshake messages (if in that phase) /// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() { APIError APINoiseFrameHelper::loop() {
APIError err = state_action_(); APIError err = state_action_();
if (err == APIError::WOULD_BLOCK) if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return APIError::OK;
if (err != APIError::OK)
return err; return err;
if (!tx_buf_.empty()) { }
if (!this->tx_buf_.empty()) {
err = try_send_tx_buf_(); err = try_send_tx_buf_();
if (err != APIError::OK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err; return err;
} }
} }
return APIError::OK; return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
} }
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@ -270,8 +310,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
// read header // read header
if (rx_header_buf_len_ < 3) { if (rx_header_buf_len_ < 3) {
// no header information yet // no header information yet
size_t to_read = 3 - rx_header_buf_len_; uint8_t to_read = 3 - rx_header_buf_len_;
ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
@ -284,8 +324,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed"); HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED; return APIError::CONNECTION_CLOSED;
} }
rx_header_buf_len_ += received; rx_header_buf_len_ += static_cast<uint8_t>(received);
if ((size_t) received != to_read) { if (static_cast<uint8_t>(received) != to_read) {
// not a full read // not a full read
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
@ -317,8 +357,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
if (rx_buf_len_ < msg_size) { if (rx_buf_len_ < msg_size) {
// more data to read // more data to read
size_t to_read = msg_size - rx_buf_len_; uint16_t to_read = msg_size - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
@ -331,8 +371,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed"); HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED; return APIError::CONNECTION_CLOSED;
} }
rx_buf_len_ += received; rx_buf_len_ += static_cast<uint16_t>(received);
if ((size_t) received != to_read) { if (static_cast<uint16_t>(received) != to_read) {
// not all read // not all read
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
@ -381,6 +421,8 @@ APIError APINoiseFrameHelper::state_action_() {
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
// ignore contents, may be used in future for flags // ignore contents, may be used in future for flags
// Reserve space for: existing prologue + 2 size bytes + frame data
prologue_.reserve(prologue_.size() + 2 + frame.msg.size());
prologue_.push_back((uint8_t) (frame.msg.size() >> 8)); prologue_.push_back((uint8_t) (frame.msg.size() >> 8));
prologue_.push_back((uint8_t) frame.msg.size()); prologue_.push_back((uint8_t) frame.msg.size());
prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end()); prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
@ -389,16 +431,20 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::SERVER_HELLO) { if (state_ == State::SERVER_HELLO) {
// send server hello // send server hello
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
std::vector<uint8_t> msg; std::vector<uint8_t> msg;
// Reserve space for: 1 byte proto + name + null + mac + null
msg.reserve(1 + name.size() + 1 + mac.size() + 1);
// chosen proto // chosen proto
msg.push_back(0x01); msg.push_back(0x01);
// node name, terminated by null byte // node name, terminated by null byte
const std::string &name = App.get_name();
const uint8_t *name_ptr = reinterpret_cast<const uint8_t *>(name.c_str()); const uint8_t *name_ptr = reinterpret_cast<const uint8_t *>(name.c_str());
msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1); msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1);
// node mac, terminated by null byte // node mac, terminated by null byte
const std::string &mac = get_mac_address();
const uint8_t *mac_ptr = reinterpret_cast<const uint8_t *>(mac.c_str()); const uint8_t *mac_ptr = reinterpret_cast<const uint8_t *>(mac.c_str());
msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1); msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1);
@ -493,16 +539,18 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
std::vector<uint8_t> data; std::vector<uint8_t> data;
data.resize(reason.length() + 1); data.resize(reason.length() + 1);
data[0] = 0x01; // failure data[0] = 0x01; // failure
for (size_t i = 0; i < reason.length(); i++) {
data[i + 1] = (uint8_t) reason[i]; // Copy error message in bulk
if (!reason.empty()) {
std::memcpy(data.data() + 1, reason.c_str(), reason.length());
} }
// temporarily remove failed state // temporarily remove failed state
auto orig_state = state_; auto orig_state = state_;
state_ = State::EXPLICIT_REJECT; state_ = State::EXPLICIT_REJECT;
write_frame_(data.data(), data.size()); write_frame_(data.data(), data.size());
state_ = orig_state; state_ = orig_state;
} }
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err; int err;
APIError aerr; APIError aerr;
@ -530,7 +578,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::CIPHERSTATE_DECRYPT_FAILED; return APIError::CIPHERSTATE_DECRYPT_FAILED;
} }
size_t msg_size = mbuf.size; uint16_t msg_size = mbuf.size;
uint8_t *msg_data = frame.msg.data(); uint8_t *msg_data = frame.msg.data();
if (msg_size < 4) { if (msg_size < 4) {
state_ = State::FAILED; state_ = State::FAILED;
@ -556,8 +604,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type; buffer->type = type;
return APIError::OK; return APIError::OK;
} }
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
int err; int err;
APIError aerr; APIError aerr;
aerr = state_action_(); aerr = state_action_();
@ -569,31 +616,36 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
size_t padding = 0; std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
size_t msg_len = 4 + payload_len + padding; // Message data starts after padding
size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_); uint16_t payload_len = raw_buffer->size() - frame_header_padding_;
auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]}; uint16_t padding = 0;
if (tmpbuf == nullptr) { uint16_t msg_len = 4 + payload_len + padding;
HELPER_LOG("Could not allocate for writing packet");
return APIError::OUT_OF_MEMORY;
}
tmpbuf[0] = 0x01; // indicator // We need to resize to include MAC space, but we already reserved it in create_buffer
// tmpbuf[1], tmpbuf[2] to be set later raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
// Write the noise header in the padded area
// Buffer layout:
// [0] - 0x01 indicator byte
// [1-2] - Size of encrypted payload (filled after encryption)
// [3-4] - Message type (encrypted)
// [5-6] - Payload length (encrypted)
// [7...] - Actual payload data (encrypted)
uint8_t *buf_start = raw_buffer->data();
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set later after encryption
const uint8_t msg_offset = 3; const uint8_t msg_offset = 3;
const uint8_t payload_offset = msg_offset + 4; buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte
tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8); // type buf_start[msg_offset + 1] = (uint8_t) type; // type low byte
tmpbuf[msg_offset + 1] = (uint8_t) type; buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte
tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte
tmpbuf[msg_offset + 3] = (uint8_t) payload_len; // payload data is already in the buffer starting at position 7
// copy data
std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
// fill padding with zeros
std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset); // The capacity parameter should be msg_len + frame_footer_size_ (MAC length) to allow space for encryption
noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
err = noise_cipherstate_encrypt(send_cipher_, &mbuf); err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
if (err != 0) { if (err != 0) {
state_ = State::FAILED; state_ = State::FAILED;
@ -601,38 +653,20 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
return APIError::CIPHERSTATE_ENCRYPT_FAILED; return APIError::CIPHERSTATE_ENCRYPT_FAILED;
} }
size_t total_len = 3 + mbuf.size; uint16_t total_len = 3 + mbuf.size;
tmpbuf[1] = (uint8_t) (mbuf.size >> 8); buf_start[1] = (uint8_t) (mbuf.size >> 8);
tmpbuf[2] = (uint8_t) mbuf.size; buf_start[2] = (uint8_t) mbuf.size;
struct iovec iov; struct iovec iov;
iov.iov_base = &tmpbuf[0]; // Point iov_base to the beginning of the buffer (no unused padding in Noise)
// We send the entire frame: indicator + size + encrypted(type + data_len + payload + MAC)
iov.iov_base = buf_start;
iov.iov_len = total_len; iov.iov_len = total_len;
// write raw to not have two packets sent if NAGLE disabled // write raw to not have two packets sent if NAGLE disabled
return write_raw_(&iov, 1); return this->write_raw_(&iov, 1);
} }
APIError APINoiseFrameHelper::try_send_tx_buf_() { APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (sent == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN)
break;
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if (sent == 0) {
break;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
}
return APIError::OK;
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
uint8_t header[3]; uint8_t header[3];
header[0] = 0x01; // indicator header[0] = 0x01; // indicator
header[1] = (uint8_t) (len >> 8); header[1] = (uint8_t) (len >> 8);
@ -642,12 +676,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
iov[0].iov_base = header; iov[0].iov_base = header;
iov[0].iov_len = 3; iov[0].iov_len = 3;
if (len == 0) { if (len == 0) {
return write_raw_(iov, 1); return this->write_raw_(iov, 1);
} }
iov[1].iov_base = const_cast<uint8_t *>(data); iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len; iov[1].iov_len = len;
return write_raw_(iov, 2); return this->write_raw_(iov, 2);
} }
/** Initiate the data structures for the handshake. /** Initiate the data structures for the handshake.
@ -718,6 +752,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
return APIError::HANDSHAKESTATE_SPLIT_FAILED; return APIError::HANDSHAKESTATE_SPLIT_FAILED;
} }
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!"); HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_); noise_handshakestate_free(handshake_);
handshake_ = nullptr; handshake_ = nullptr;
@ -740,22 +776,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
} }
} }
APIError APINoiseFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APINoiseFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
extern "C" { extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system) // declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) { void noise_rand_bytes(void *output, size_t len) {
@ -766,32 +786,15 @@ void noise_rand_bytes(void *output, size_t len) {
} }
} }
// Explicit template instantiation for Noise
template APIError APIFrameHelper::write_raw_<APINoiseFrameHelper::State>(
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
APINoiseFrameHelper::State &state, APINoiseFrameHelper::State failed_state);
#endif // USE_API_NOISE #endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT #ifdef USE_API_PLAINTEXT
/// Initialize the frame helper, returns OK if successful. /// Initialize the frame helper, returns OK if successful.
APIError APIPlaintextFrameHelper::init() { APIError APIPlaintextFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) { APIError err = init_common_();
HELPER_LOG("Bad state for init %d", (int) state_); if (err != APIError::OK) {
return APIError::BAD_STATE; return err;
}
int err = socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED;
} }
state_ = State::DATA; state_ = State::DATA;
@ -802,14 +805,13 @@ APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) { if (state_ != State::DATA) {
return APIError::BAD_STATE; return APIError::BAD_STATE;
} }
// try send pending TX data if (!this->tx_buf_.empty()) {
if (!tx_buf_.empty()) {
APIError err = try_send_tx_buf_(); APIError err = try_send_tx_buf_();
if (err != APIError::OK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err; return err;
} }
} }
return APIError::OK; return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
} }
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@ -834,7 +836,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// there is no data on the wire (which is the common case). // there is no data on the wire (which is the common case).
// This results in faster failure detection compared to // This results in faster failure detection compared to
// attempting to read multiple bytes at once. // attempting to read multiple bytes at once.
ssize_t received = socket_->read(&data, 1); ssize_t received = this->socket_->read(&data, 1);
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
@ -898,14 +900,24 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
continue; continue;
} }
rx_header_parsed_len_ = msg_size_varint->as_uint32(); if (msg_size_varint->as_uint32() > 65535) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum 65535", msg_size_varint->as_uint32());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed); auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed);
if (!msg_type_varint.has_value()) { if (!msg_type_varint.has_value()) {
// not enough data there yet // not enough data there yet
continue; continue;
} }
rx_header_parsed_type_ = msg_type_varint->as_uint32(); if (msg_type_varint->as_uint32() > 65535) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum 65535", msg_type_varint->as_uint32());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_type_ = msg_type_varint->as_uint16();
rx_header_parsed_ = true; rx_header_parsed_ = true;
} }
// header reading done // header reading done
@ -917,8 +929,8 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
if (rx_buf_len_ < rx_header_parsed_len_) { if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read // more data to read
size_t to_read = rx_header_parsed_len_ - rx_buf_len_; uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
@ -931,8 +943,8 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed"); HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED; return APIError::CONNECTION_CLOSED;
} }
rx_buf_len_ += received; rx_buf_len_ += static_cast<uint16_t>(received);
if ((size_t) received != to_read) { if (static_cast<uint16_t>(received) != to_read) {
// not all read // not all read
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
@ -950,7 +962,6 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
rx_header_parsed_ = false; rx_header_parsed_ = false;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr; APIError aerr;
@ -978,7 +989,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
"Bad indicator byte"; "Bad indicator byte";
iov[0].iov_base = (void *) msg; iov[0].iov_base = (void *) msg;
iov[0].iov_len = 19; iov[0].iov_len = 19;
write_raw_(iov, 1); this->write_raw_(iov, 1);
} }
return aerr; return aerr;
} }
@ -989,70 +1000,68 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = rx_header_parsed_type_; buffer->type = rx_header_parsed_type_;
return APIError::OK; return APIError::OK;
} }
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
if (state_ != State::DATA) { if (state_ != State::DATA) {
return APIError::BAD_STATE; return APIError::BAD_STATE;
} }
std::vector<uint8_t> header; std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
header.reserve(1 + api::ProtoSize::varint(static_cast<uint32_t>(payload_len)) + // Message data starts after padding (frame_header_padding_ = 6)
api::ProtoSize::varint(static_cast<uint32_t>(type))); uint16_t payload_len = static_cast<uint16_t>(raw_buffer->size() - frame_header_padding_);
header.push_back(0x00);
ProtoVarInt(payload_len).encode(header);
ProtoVarInt(type).encode(header);
struct iovec iov[2]; // Calculate varint sizes for header components
iov[0].iov_base = &header[0]; uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
iov[0].iov_len = header.size(); uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
if (payload_len == 0) { uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
return write_raw_(iov, 1);
}
iov[1].iov_base = const_cast<uint8_t *>(payload);
iov[1].iov_len = payload_len;
return write_raw_(iov, 2); if (total_header_len > frame_header_padding_) {
} // Header is too large to fit in the padding
APIError APIPlaintextFrameHelper::try_send_tx_buf_() { return APIError::BAD_ARG;
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (is_would_block(sent)) {
break;
} else if (sent == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
} }
return APIError::OK; // Calculate where to start writing the header
// The header starts at the latest possible position to minimize unused padding
//
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
// [0-2] - Unused padding
// [3] - 0x00 indicator byte
// [4] - Payload size varint (1 byte, for sizes 0-127)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
// [0-1] - Unused padding
// [2] - 0x00 indicator byte
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
// [4-5] - Message type varint (2 bytes, for types 128-32767)
// [6...] - Actual payload data
uint8_t *buf_start = raw_buffer->data();
uint8_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode size varint directly into buffer
ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
// Encode type varint directly into buffer
ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
struct iovec iov;
// Point iov_base to the beginning of our header (skip unused padding)
// This ensures we only send the actual header and payload, not the empty padding bytes
iov.iov_base = buf_start + header_offset;
iov.iov_len = total_header_len + payload_len;
return write_raw_(&iov, 1);
} }
APIError APIPlaintextFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
// Explicit template instantiation for Plaintext
template APIError APIFrameHelper::write_raw_<APIPlaintextFrameHelper::State>(
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
APIPlaintextFrameHelper::State &state, APIPlaintextFrameHelper::State failed_state);
#endif // USE_API_PLAINTEXT #endif // USE_API_PLAINTEXT
} // namespace api } // namespace api

View File

@ -16,18 +16,13 @@
namespace esphome { namespace esphome {
namespace api { namespace api {
class ProtoWriteBuffer;
struct ReadPacketBuffer { struct ReadPacketBuffer {
std::vector<uint8_t> container; std::vector<uint8_t> container;
uint16_t type; uint16_t type;
size_t data_offset; uint16_t data_offset;
size_t data_len; uint16_t data_len;
};
struct PacketBuffer {
const std::vector<uint8_t> container;
uint16_t type;
uint8_t data_offset;
uint8_t data_len;
}; };
enum class APIError : int { enum class APIError : int {
@ -60,74 +55,147 @@ const char *api_error_to_str(APIError err);
class APIFrameHelper { class APIFrameHelper {
public: public:
APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) {
socket_ = socket_owned_.get();
}
virtual ~APIFrameHelper() = default; virtual ~APIFrameHelper() = default;
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop() = 0; virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
virtual bool can_write_without_blocking() = 0; bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0; std::string getpeername() { return socket_->getpeername(); }
virtual std::string getpeername() = 0; int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; APIError close() {
virtual APIError close() = 0; state_ = State::CLOSED;
virtual APIError shutdown(int how) = 0; int err = this->socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError shutdown(int how) {
int err = this->socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
// Give this helper a name for logging // Give this helper a name for logging
virtual void set_log_info(std::string info) = 0; void set_log_info(std::string info) { info_ = std::move(info); }
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
// Get the frame header padding required by this protocol
virtual uint8_t frame_header_padding() = 0;
// Get the frame footer size required by this protocol
virtual uint8_t frame_footer_size() = 0;
protected: protected:
// Struct for holding parsed frame data
struct ParsedFrame {
std::vector<uint8_t> msg;
};
// Buffer containing data to be sent
struct SendBuffer {
std::vector<uint8_t> data;
uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage)
// Using uint16_t reduces memory usage since ESPHome API messages are limited to 64KB max
uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; }
const uint8_t *current_data() const { return data.data() + offset; }
};
// Queue of data buffers to be sent
std::deque<SendBuffer> tx_buf_;
// Common state enum for all frame helpers
// Note: Not all states are used by all implementations
// - INITIALIZE: Used by both Noise and Plaintext
// - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
// - DATA: Used by both Noise and Plaintext
// - CLOSED: Used by both Noise and Plaintext
// - FAILED: Used by both Noise and Plaintext
// - EXPLICIT_REJECT: Only used by Noise protocol
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2, // Noise only
SERVER_HELLO = 3, // Noise only
HANDSHAKE = 4, // Noise only
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8, // Noise only
};
// Current state of the frame helper
State state_{State::INITIALIZE};
// Helper name for logging
std::string info_;
// Socket for communication
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common implementation for writing raw data to socket // Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt);
// Try to send data from the tx buffer
APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
template<typename StateEnum> template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf, APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state); const std::string &info, StateEnum &state, StateEnum failed_state);
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
// Receive buffer for reading frame data
std::vector<uint8_t> rx_buf_;
uint16_t rx_buf_len_ = 0;
// Common initialization for both plaintext and noise protocols
APIError init_common_();
}; };
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
class APINoiseFrameHelper : public APIFrameHelper { class APINoiseFrameHelper : public APIFrameHelper {
public: public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx) APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
: socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {} : APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
}
~APINoiseFrameHelper() override; ~APINoiseFrameHelper() override;
APIError init() override; APIError init() override;
APIError loop() override; APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; // Get the frame header padding required by this protocol
std::string getpeername() override { return this->socket_->getpeername(); } uint8_t frame_header_padding() override { return frame_header_padding_; }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { // Get the frame footer size required by this protocol
return this->socket_->getpeername(addr, addrlen); uint8_t frame_footer_size() override { return frame_footer_size_; }
}
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
protected: protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError state_action_(); APIError state_action_();
APIError try_read_frame_(ParsedFrame *frame); APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_(); APIError write_frame_(const uint8_t *data, uint16_t len);
APIError write_frame_(const uint8_t *data, size_t len);
inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
}
APIError init_handshake_(); APIError init_handshake_();
APIError check_handshake_finished_(); APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason); void send_explicit_handshake_reject_(const std::string &reason);
std::unique_ptr<socket::Socket> socket_;
std::string info_;
// Fixed-size header buffer for noise protocol: // Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase // Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3]; uint8_t rx_header_buf_[3];
size_t rx_header_buf_len_ = 0; uint8_t rx_header_buf_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
std::vector<uint8_t> prologue_; std::vector<uint8_t> prologue_;
std::shared_ptr<APINoiseContext> ctx_; std::shared_ptr<APINoiseContext> ctx_;
@ -135,53 +203,31 @@ class APINoiseFrameHelper : public APIFrameHelper {
NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr};
NoiseProtocolId nid_; NoiseProtocolId nid_;
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2,
SERVER_HELLO = 3,
HANDSHAKE = 4,
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8,
} state_ = State::INITIALIZE;
}; };
#endif // USE_API_NOISE #endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT #ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper { class APIPlaintextFrameHelper : public APIFrameHelper {
public: public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {} APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
// 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)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
}
~APIPlaintextFrameHelper() override = default; ~APIPlaintextFrameHelper() override = default;
APIError init() override; APIError init() override;
APIError loop() override; APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; uint8_t frame_header_padding() override { return frame_header_padding_; }
std::string getpeername() override { return this->socket_->getpeername(); } // Get the frame footer size required by this protocol
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { uint8_t frame_footer_size() override { return frame_footer_size_; }
return this->socket_->getpeername(addr, addrlen);
}
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
protected: protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError try_read_frame_(ParsedFrame *frame); APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_();
inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
}
std::unique_ptr<socket::Socket> socket_;
std::string info_;
// Fixed-size header buffer for plaintext protocol: // Fixed-size header buffer for plaintext protocol:
// We only need space for the two varints since we validate the indicator byte separately. // We only need space for the two varints since we validate the indicator byte separately.
// To match noise protocol's maximum message size (65535), we need: // To match noise protocol's maximum message size (65535), we need:
@ -193,20 +239,8 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type) uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0; uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false; bool rx_header_parsed_ = false;
uint32_t rx_header_parsed_type_ = 0; uint16_t rx_header_parsed_type_ = 0;
uint32_t rx_header_parsed_len_ = 0; uint16_t rx_header_parsed_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
enum class State {
INITIALIZE = 1,
DATA = 2,
CLOSED = 3,
FAILED = 4,
} state_ = State::INITIALIZE;
}; };
#endif #endif

View File

@ -55,6 +55,7 @@ class ProtoVarInt {
return {}; // Incomplete or invalid varint return {}; // Incomplete or invalid varint
} }
uint16_t as_uint16() const { return this->value_; }
uint32_t as_uint32() const { return this->value_; } uint32_t as_uint32() const { return this->value_; }
uint64_t as_uint64() const { return this->value_; } uint64_t as_uint64() const { return this->value_; }
bool as_bool() const { return this->value_; } bool as_bool() const { return this->value_; }
@ -83,6 +84,34 @@ class ProtoVarInt {
return static_cast<int64_t>(this->value_ >> 1); return static_cast<int64_t>(this->value_ >> 1);
} }
} }
/**
* Encode the varint value to a pre-allocated buffer without bounds checking.
*
* @param buffer The pre-allocated buffer to write the encoded varint to
* @param len The size of the buffer in bytes
*
* @note The caller is responsible for ensuring the buffer is large enough
* to hold the encoded value. Use ProtoSize::varint() to calculate
* the exact size needed before calling this method.
* @note No bounds checking is performed for performance reasons.
*/
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
uint64_t val = this->value_;
if (val <= 0x7F) {
buffer[0] = val;
return;
}
size_t i = 0;
while (val && i < len) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
buffer[i++] = temp | 0x80;
} else {
buffer[i++] = temp;
}
}
}
void encode(std::vector<uint8_t> &out) { void encode(std::vector<uint8_t> &out) {
uint64_t val = this->value_; uint64_t val = this->value_;
if (val <= 0x7F) { if (val <= 0x7F) {

View File

@ -14,11 +14,8 @@ namespace esphome {
namespace at581x { namespace at581x {
class AT581XComponent : public Component, public i2c::I2CDevice { class AT581XComponent : public Component, public i2c::I2CDevice {
#ifdef USE_SWITCH
protected:
switch_::Switch *rf_power_switch_{nullptr};
public: public:
#ifdef USE_SWITCH
void set_rf_power_switch(switch_::Switch *s) { void set_rf_power_switch(switch_::Switch *s) {
this->rf_power_switch_ = s; this->rf_power_switch_ = s;
s->turn_on(); s->turn_on();
@ -48,6 +45,9 @@ class AT581XComponent : public Component, public i2c::I2CDevice {
bool i2c_read_reg(uint8_t addr, uint8_t &data); bool i2c_read_reg(uint8_t addr, uint8_t &data);
protected: protected:
#ifdef USE_SWITCH
switch_::Switch *rf_power_switch_{nullptr};
#endif
int freq_; int freq_;
int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */ int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */
int protect_time_ms_; /*!< Protection time, recommended 1000 ms */ int protect_time_ms_; /*!< Protection time, recommended 1000 ms */

View File

@ -7,7 +7,7 @@ CODEOWNERS = ["@bazuchan"]
ballu_ns = cg.esphome_ns.namespace("ballu") ballu_ns = cg.esphome_ns.namespace("ballu")
BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR) BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(BalluClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate)
async def to_code(config): async def to_code(config):

View File

@ -3,6 +3,7 @@
#include "bedjet_hub.h" #include "bedjet_hub.h"
#include "bedjet_child.h" #include "bedjet_child.h"
#include "bedjet_const.h" #include "bedjet_const.h"
#include "esphome/core/application.h"
#include <cinttypes> #include <cinttypes>
namespace esphome { namespace esphome {

View File

@ -15,17 +15,21 @@ void BinarySensor::publish_state(bool state) {
if (!this->publish_dedup_.next(state)) if (!this->publish_dedup_.next(state))
return; return;
if (this->filter_list_ == nullptr) { if (this->filter_list_ == nullptr) {
this->send_state_internal(state); this->send_state_internal(state, false);
} else { } else {
this->filter_list_->input(state); this->filter_list_->input(state, false);
} }
} }
void BinarySensor::publish_initial_state(bool state) { void BinarySensor::publish_initial_state(bool state) {
this->has_state_ = false; if (!this->publish_dedup_.next(state))
this->publish_state(state); return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, true);
} else {
this->filter_list_->input(state, true);
}
} }
void BinarySensor::send_state_internal(bool state) { void BinarySensor::send_state_internal(bool state, bool is_initial) {
bool is_initial = !this->has_state_;
if (is_initial) { if (is_initial) {
ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state)); ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
} else { } else {

View File

@ -67,7 +67,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
// (In most use cases you won't need these) // (In most use cases you won't need these)
void send_state_internal(bool state); void send_state_internal(bool state, bool is_initial);
/// Return whether this binary sensor has outputted a state. /// Return whether this binary sensor has outputted a state.
virtual bool has_state() const; virtual bool has_state() const;

View File

@ -9,37 +9,37 @@ namespace binary_sensor {
static const char *const TAG = "sensor.filter"; static const char *const TAG = "sensor.filter";
void Filter::output(bool value) { void Filter::output(bool value, bool is_initial) {
if (!this->dedup_.next(value)) if (!this->dedup_.next(value))
return; return;
if (this->next_ == nullptr) { if (this->next_ == nullptr) {
this->parent_->send_state_internal(value); this->parent_->send_state_internal(value, is_initial);
} else { } else {
this->next_->input(value); this->next_->input(value, is_initial);
} }
} }
void Filter::input(bool value) { void Filter::input(bool value, bool is_initial) {
auto b = this->new_value(value); auto b = this->new_value(value, is_initial);
if (b.has_value()) { if (b.has_value()) {
this->output(*b); this->output(*b, is_initial);
} }
} }
optional<bool> DelayedOnOffFilter::new_value(bool value) { optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
if (value) { if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
} else { } else {
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); }); this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
} }
return {}; return {};
} }
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOnFilter::new_value(bool value) { optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
if (value) { if (value) {
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); }); this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
return {}; return {};
} else { } else {
this->cancel_timeout("ON"); this->cancel_timeout("ON");
@ -49,9 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value) {
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOffFilter::new_value(bool value) { optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
if (!value) { if (!value) {
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); }); this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
return {}; return {};
} else { } else {
this->cancel_timeout("OFF"); this->cancel_timeout("OFF");
@ -61,11 +61,11 @@ optional<bool> DelayedOffFilter::new_value(bool value) {
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> InvertFilter::new_value(bool value) { return !value; } optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {} AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
optional<bool> AutorepeatFilter::new_value(bool value) { optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) {
if (value) { if (value) {
// Ignore if already running // Ignore if already running
if (this->active_timing_ != 0) if (this->active_timing_ != 0)
@ -101,7 +101,7 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) { void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val); this->output(val, false); // This is at least the second one so not initial
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
} }
@ -109,18 +109,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {} LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); } optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value) { optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
if (!this->steady_) { if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() { this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
this->steady_ = true; this->steady_ = true;
this->output(value); this->output(value, is_initial);
}); });
return {}; return {};
} else { } else {
this->steady_ = false; this->steady_ = false;
this->output(value); this->output(value, is_initial);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value; return value;
} }

View File

@ -14,11 +14,11 @@ class BinarySensor;
class Filter { class Filter {
public: public:
virtual optional<bool> new_value(bool value) = 0; virtual optional<bool> new_value(bool value, bool is_initial) = 0;
void input(bool value); void input(bool value, bool is_initial);
void output(bool value); void output(bool value, bool is_initial);
protected: protected:
friend BinarySensor; friend BinarySensor;
@ -30,7 +30,7 @@ class Filter {
class DelayedOnOffFilter : public Filter, public Component { class DelayedOnOffFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnFilter : public Filter, public Component { class DelayedOnFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component {
class DelayedOffFilter : public Filter, public Component { class DelayedOffFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component {
class InvertFilter : public Filter { class InvertFilter : public Filter {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
}; };
struct AutorepeatFilterTiming { struct AutorepeatFilterTiming {
@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component {
public: public:
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings); explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -102,7 +102,7 @@ class LambdaFilter : public Filter {
public: public:
explicit LambdaFilter(std::function<optional<bool>(bool)> f); explicit LambdaFilter(std::function<optional<bool>(bool)> f);
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
protected: protected:
std::function<optional<bool>(bool)> f_; std::function<optional<bool>(bool)> f_;
@ -110,7 +110,7 @@ class LambdaFilter : public Filter {
class SettleFilter : public Filter, public Component { class SettleFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;

View File

@ -2,6 +2,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
#include "esphome/core/application.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -177,7 +178,7 @@ void BluetoothProxy::loop() {
// Flush any pending BLE advertisements that have been accumulated but not yet sent // Flush any pending BLE advertisements that have been accumulated but not yet sent
if (this->raw_advertisements_) { if (this->raw_advertisements_) {
static uint32_t last_flush_time = 0; static uint32_t last_flush_time = 0;
uint32_t now = millis(); uint32_t now = App.get_loop_component_start_time();
// Flush accumulated advertisements every 100ms // Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) { if (now - last_flush_time >= 100) {

View File

@ -40,7 +40,7 @@ def climate_ir_schema(
) )
def climare_ir_with_receiver_schema( def climate_ir_with_receiver_schema(
class_: MockObjClass, class_: MockObjClass,
) -> cv.Schema: ) -> cv.Schema:
return climate_ir_schema(class_).extend( return climate_ir_schema(class_).extend(
@ -59,7 +59,7 @@ def deprecated_schema_constant(config):
type = str(id.type).split("::", maxsplit=1)[0] type = str(id.type).split("::", maxsplit=1)[0]
_LOGGER.warning( _LOGGER.warning(
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
"Please use `climate_ir.climare_ir_with_receiver_schema(...)` instead. " "Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
"If you are seeing this, report an issue to the external_component author and ask them to update it. " "If you are seeing this, report an issue to the external_component author and ask them to update it. "
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
"Component using this schema: %s", "Component using this schema: %s",
@ -68,7 +68,7 @@ def deprecated_schema_constant(config):
return config return config
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climare_ir_with_receiver_schema(ClimateIR) CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant) CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)

View File

@ -13,7 +13,7 @@ CONF_BIT_HIGH = "bit_high"
CONF_BIT_ONE_LOW = "bit_one_low" CONF_BIT_ONE_LOW = "bit_one_low"
CONF_BIT_ZERO_LOW = "bit_zero_low" CONF_BIT_ZERO_LOW = "bit_zero_low"
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(LgIrClimate).extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend(
{ {
cv.Optional( cv.Optional(
CONF_HEADER_HIGH, default="8000us" CONF_HEADER_HIGH, default="8000us"

View File

@ -0,0 +1 @@
"""CM1106 component for ESPHome."""

View File

@ -0,0 +1,112 @@
#include "cm1106.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace cm1106 {
static const char *const TAG = "cm1106";
static const uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
uint8_t crc = 0;
for (int i = 0; i < len - 1; i++) {
crc -= response[i];
}
return crc;
}
void CM1106Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up CM1106...");
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
ESP_LOGE(TAG, "Communication with CM1106 failed!");
this->mark_failed();
return;
}
}
void CM1106Component::update() {
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
this->status_set_warning();
return;
}
if (response[0] != 0x16 || response[1] != 0x05 || response[2] != 0x01) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X...", response[0], response[1], response[2],
response[3]);
this->status_set_warning();
return;
}
uint8_t checksum = cm1106_checksum(response, sizeof(response));
if (response[7] != checksum) {
ESP_LOGW(TAG, "CM1106 Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
this->status_set_warning();
return;
}
this->status_clear_warning();
uint16_t ppm = response[3] << 8 | response[4];
ESP_LOGD(TAG, "CM1106 Received CO₂=%uppm DF3=%02X DF4=%02X", ppm, response[5], response[6]);
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(ppm);
}
void CM1106Component::calibrate_zero(uint16_t ppm) {
uint8_t cmd[6];
memcpy(cmd, C_M1106_CMD_SET_CO2_CALIB, sizeof(cmd));
cmd[3] = ppm >> 8;
cmd[4] = ppm & 0xFF;
uint8_t response[4] = {0};
if (!this->cm1106_write_command_(cmd, sizeof(cmd), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
this->status_set_warning();
return;
}
// check if correct response received
if (memcmp(response, C_M1106_CMD_SET_CO2_CALIB_RESPONSE, sizeof(response)) != 0) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
response[3]);
this->status_set_warning();
return;
}
this->status_clear_warning();
ESP_LOGD(TAG, "CM1106 Successfully calibrated sensor to %uppm", ppm);
}
bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response,
size_t response_len) {
// Empty RX Buffer
while (this->available())
this->read();
this->write_array(command, command_len - 1);
this->write_byte(cm1106_checksum(command, command_len));
this->flush();
if (response == nullptr)
return true;
return this->read_array(response, response_len);
}
void CM1106Component::dump_config() {
ESP_LOGCONFIG(TAG, "CM1106:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(9600);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with CM1106 failed!");
}
}
} // namespace cm1106
} // namespace esphome

View File

@ -0,0 +1,40 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace cm1106 {
class CM1106Component : public PollingComponent, public uart::UARTDevice {
public:
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
void setup() override;
void update() override;
void dump_config() override;
void calibrate_zero(uint16_t ppm);
void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; }
protected:
sensor::Sensor *co2_sensor_{nullptr};
bool cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, size_t response_len);
};
template<typename... Ts> class CM1106CalibrateZeroAction : public Action<Ts...> {
public:
CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {}
void play(Ts... x) override { this->cm1106_->calibrate_zero(400); }
protected:
CM1106Component *cm1106_;
};
} // namespace cm1106
} // namespace esphome

View File

@ -0,0 +1,72 @@
"""CM1106 Sensor component for ESPHome."""
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.components import sensor, uart
from esphome.const import (
CONF_CO2,
CONF_ID,
DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
)
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@andrewjswan"]
cm1106_ns = cg.esphome_ns.namespace("cm1106")
CM1106Component = cm1106_ns.class_(
"CM1106Component", cg.PollingComponent, uart.UARTDevice
)
CM1106CalibrateZeroAction = cm1106_ns.class_(
"CM1106CalibrateZeroAction",
automation.Action,
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(CM1106Component),
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
},
)
.extend(cv.polling_component_schema("60s"))
.extend(uart.UART_DEVICE_SCHEMA)
)
async def to_code(config) -> None:
"""Code generation entry point."""
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if co2_config := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2_config)
cg.add(var.set_co2_sensor(sens))
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(CM1106Component),
},
)
@automation.register_action(
"cm1106.calibrate_zero",
CM1106CalibrateZeroAction,
CALIBRATION_ACTION_SCHEMA,
)
async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None:
"""Service code generation entry point."""
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)

View File

@ -7,7 +7,7 @@ CODEOWNERS = ["@glmnet"]
coolix_ns = cg.esphome_ns.namespace("coolix") coolix_ns = cg.esphome_ns.namespace("coolix")
CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR) CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(CoolixClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
async def to_code(config): async def to_code(config):

View File

@ -1,5 +1,6 @@
#include "cse7766.h" #include "cse7766.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace cse7766 { namespace cse7766 {
@ -7,7 +8,7 @@ namespace cse7766 {
static const char *const TAG = "cse7766"; static const char *const TAG = "cse7766";
void CSE7766Component::loop() { void CSE7766Component::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) { if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index. // last transmission too long ago. Reset RX index.
this->raw_data_index_ = 0; this->raw_data_index_ = 0;

View File

@ -1,6 +1,7 @@
#include "current_based_cover.h" #include "current_based_cover.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cfloat> #include <cfloat>
namespace esphome { namespace esphome {
@ -60,7 +61,7 @@ void CurrentBasedCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE) if (this->current_operation == COVER_OPERATION_IDLE)
return; return;
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (this->current_operation == COVER_OPERATION_OPENING) { if (this->current_operation == COVER_OPERATION_OPENING) {
if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction

View File

@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
daikin_ns = cg.esphome_ns.namespace("daikin") daikin_ns = cg.esphome_ns.namespace("daikin")
DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR) DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinClimate)
async def to_code(config): async def to_code(config):

View File

@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc") daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc")
DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR) DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinArcClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinArcClimate)
async def to_code(config): async def to_code(config):

View File

@ -9,7 +9,7 @@ daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc")
DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR) DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinBrcClimate).extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinBrcClimate).extend(
{ {
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
} }

View File

@ -1,6 +1,7 @@
#include "daly_bms.h" #include "daly_bms.h"
#include <vector> #include <vector>
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace daly_bms { namespace daly_bms {
@ -32,7 +33,7 @@ void DalyBmsComponent::update() {
} }
void DalyBmsComponent::loop() { void DalyBmsComponent::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (this->receiving_ && (now - this->last_transmission_ >= 200)) { if (this->receiving_ && (now - this->last_transmission_ >= 200)) {
// last transmission too long ago. Reset RX index. // last transmission too long ago. Reset RX index.
ESP_LOGW(TAG, "Last transmission too long ago. Reset RX index."); ESP_LOGW(TAG, "Last transmission too long ago. Reset RX index.");

View File

@ -2,7 +2,6 @@ import base64
from pathlib import Path from pathlib import Path
import re import re
import secrets import secrets
from typing import Optional
import requests import requests
from ruamel.yaml import YAML from ruamel.yaml import YAML
@ -84,7 +83,7 @@ async def to_code(config):
def import_config( def import_config(
path: str, path: str,
name: str, name: str,
friendly_name: Optional[str], friendly_name: str | None,
project_name: str, project_name: str,
import_url: str, import_url: str,
network: str = CONF_WIFI, network: str = CONF_WIFI,

View File

@ -70,7 +70,7 @@ void DebugComponent::loop() {
#ifdef USE_SENSOR #ifdef USE_SENSOR
// calculate loop time - from last call to this one // calculate loop time - from last call to this one
if (this->loop_time_sensor_ != nullptr) { if (this->loop_time_sensor_ != nullptr) {
uint32_t now = millis(); uint32_t now = App.get_loop_component_start_time();
uint32_t loop_time = now - this->last_loop_timetag_; uint32_t loop_time = now - this->last_loop_timetag_;
this->max_loop_time_ = std::max(this->max_loop_time_, loop_time); this->max_loop_time_ = std::max(this->max_loop_time_, loop_time);
this->last_loop_timetag_ = now; this->last_loop_timetag_ = now;

View File

@ -34,13 +34,15 @@ class DebugComponent : public PollingComponent {
#endif #endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32 #ifdef USE_ESP32
void on_shutdown() override;
void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; } void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; }
#endif // USE_ESP32 #endif // USE_ESP32
void set_cpu_frequency_sensor(sensor::Sensor *cpu_frequency_sensor) { void set_cpu_frequency_sensor(sensor::Sensor *cpu_frequency_sensor) {
this->cpu_frequency_sensor_ = cpu_frequency_sensor; this->cpu_frequency_sensor_ = cpu_frequency_sensor;
} }
#endif // USE_SENSOR #endif // USE_SENSOR
#ifdef USE_ESP32
void on_shutdown() override;
#endif // USE_ESP32
protected: protected:
uint32_t free_heap_{}; uint32_t free_heap_{};

View File

@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
delonghi_ns = cg.esphome_ns.namespace("delonghi") delonghi_ns = cg.esphome_ns.namespace("delonghi")
DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DelonghiClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DelonghiClimate)
async def to_code(config): async def to_code(config):

View File

@ -7,7 +7,7 @@ AUTO_LOAD = ["climate_ir"]
emmeti_ns = cg.esphome_ns.namespace("emmeti") emmeti_ns = cg.esphome_ns.namespace("emmeti")
EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR) EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(EmmetiClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(EmmetiClimate)
async def to_code(config): async def to_code(config):

View File

@ -1,6 +1,7 @@
#include "endstop_cover.h" #include "endstop_cover.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace endstop { namespace endstop {
@ -65,7 +66,7 @@ void EndstopCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE) if (this->current_operation == COVER_OPERATION_IDLE)
return; return;
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (this->current_operation == COVER_OPERATION_OPENING && this->is_open_()) { if (this->current_operation == COVER_OPERATION_OPENING && this->is_open_()) {
float dur = (now - this->start_dir_time_) / 1e3f; float dur = (now - this->start_dir_time_) / 1e3f;

View File

@ -3,7 +3,6 @@ import itertools
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from typing import Optional, Union
from esphome import git from esphome import git
import esphome.codegen as cg import esphome.codegen as cg
@ -60,6 +59,7 @@ from .const import ( # noqa
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
VARIANT_FRIENDLY, VARIANT_FRIENDLY,
@ -90,6 +90,7 @@ CPU_FREQUENCIES = {
VARIANT_ESP32C3: get_cpu_frequencies(80, 160), VARIANT_ESP32C3: get_cpu_frequencies(80, 160),
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160), VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96), VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96),
VARIANT_ESP32P4: get_cpu_frequencies(40, 360, 400),
} }
# Make sure not missed here if a new variant added. # Make sure not missed here if a new variant added.
@ -189,7 +190,7 @@ class RawSdkconfigValue:
value: str value: str
SdkconfigValueType = Union[bool, int, HexInt, str, RawSdkconfigValue] SdkconfigValueType = bool | int | HexInt | str | RawSdkconfigValue
def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
@ -206,8 +207,8 @@ def add_idf_component(
ref: str = None, ref: str = None,
path: str = None, path: str = None,
refresh: TimePeriod = None, refresh: TimePeriod = None,
components: Optional[list[str]] = None, components: list[str] | None = None,
submodules: Optional[list[str]] = None, submodules: list[str] | None = None,
): ):
"""Add an esp-idf component to the project.""" """Add an esp-idf component to the project."""
if not CORE.using_esp_idf: if not CORE.using_esp_idf:
@ -296,11 +297,11 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
# The default/recommended esp-idf framework version # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 1, 6) RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 3, 2)
# The platformio/espressif32 version to use for esp-idf frameworks # The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases # - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
ESP_IDF_PLATFORM_VERSION = cv.Version(51, 3, 7) ESP_IDF_PLATFORM_VERSION = cv.Version(53, 3, 13)
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions # List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
@ -369,8 +370,8 @@ def _arduino_check_versions(value):
def _esp_idf_check_versions(value): def _esp_idf_check_versions(value):
value = value.copy() value = value.copy()
lookups = { lookups = {
"dev": (cv.Version(5, 1, 6), "https://github.com/espressif/esp-idf.git"), "dev": (cv.Version(5, 3, 2), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 1, 6), None), "latest": (cv.Version(5, 3, 2), None),
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
} }

View File

@ -4,6 +4,7 @@ from .const import (
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
@ -1632,6 +1633,14 @@ BOARDS = {
"name": "Espressif ESP32-H2-DevKit", "name": "Espressif ESP32-H2-DevKit",
"variant": VARIANT_ESP32H2, "variant": VARIANT_ESP32H2,
}, },
"esp32-p4": {
"name": "Espressif ESP32-P4 generic",
"variant": VARIANT_ESP32P4,
},
"esp32-p4-evboard": {
"name": "Espressif ESP32-P4 Function EV Board",
"variant": VARIANT_ESP32P4,
},
"esp32-pico-devkitm-2": { "esp32-pico-devkitm-2": {
"name": "Espressif ESP32-PICO-DevKitM-2", "name": "Espressif ESP32-PICO-DevKitM-2",
"variant": VARIANT_ESP32, "variant": VARIANT_ESP32,

View File

@ -19,6 +19,7 @@ VARIANT_ESP32C2 = "ESP32C2"
VARIANT_ESP32C3 = "ESP32C3" VARIANT_ESP32C3 = "ESP32C3"
VARIANT_ESP32C6 = "ESP32C6" VARIANT_ESP32C6 = "ESP32C6"
VARIANT_ESP32H2 = "ESP32H2" VARIANT_ESP32H2 = "ESP32H2"
VARIANT_ESP32P4 = "ESP32P4"
VARIANTS = [ VARIANTS = [
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32S2, VARIANT_ESP32S2,
@ -27,6 +28,7 @@ VARIANTS = [
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
] ]
VARIANT_FRIENDLY = { VARIANT_FRIENDLY = {
@ -37,6 +39,7 @@ VARIANT_FRIENDLY = {
VARIANT_ESP32C3: "ESP32-C3", VARIANT_ESP32C3: "ESP32-C3",
VARIANT_ESP32C6: "ESP32-C6", VARIANT_ESP32C6: "ESP32-C6",
VARIANT_ESP32H2: "ESP32-H2", VARIANT_ESP32H2: "ESP32-H2",
VARIANT_ESP32P4: "ESP32-P4",
} }
esp32_ns = cg.esphome_ns.namespace("esp32") esp32_ns = cg.esphome_ns.namespace("esp32")

View File

@ -1,6 +1,7 @@
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from typing import Any, Callable from typing import Any
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
@ -28,6 +29,7 @@ from .const import (
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
esp32_ns, esp32_ns,
@ -37,6 +39,7 @@ from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_support
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports
@ -105,6 +108,10 @@ _esp32_validations = {
pin_validation=esp32_h2_validate_gpio_pin, pin_validation=esp32_h2_validate_gpio_pin,
usage_validation=esp32_h2_validate_supports, usage_validation=esp32_h2_validate_supports,
), ),
VARIANT_ESP32P4: ESP32ValidationFunctions(
pin_validation=esp32_p4_validate_gpio_pin,
usage_validation=esp32_p4_validate_supports,
),
VARIANT_ESP32S2: ESP32ValidationFunctions( VARIANT_ESP32S2: ESP32ValidationFunctions(
pin_validation=esp32_s2_validate_gpio_pin, pin_validation=esp32_s2_validate_gpio_pin,
usage_validation=esp32_s2_validate_supports, usage_validation=esp32_s2_validate_supports,

View File

@ -0,0 +1,43 @@
import logging
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
_ESP32P4_USB_JTAG_PINS = {24, 25}
_ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38}
_LOGGER = logging.getLogger(__name__)
def esp32_p4_validate_gpio_pin(value):
if value < 0 or value > 54:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
if value in _ESP32P4_STRAPPING_PINS:
_LOGGER.warning(
"GPIO%d is a Strapping PIN and should be avoided.\n"
"Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
value,
)
if value in _ESP32P4_USB_JTAG_PINS:
_LOGGER.warning(
"GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
"To use this pin as GPIO, USB-Serial-JTAG will be disabled.",
value,
)
return value
def esp32_p4_validate_supports(value):
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 54:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
if is_input:
# All ESP32 pins support input mode
pass
return value

View File

@ -6,6 +6,7 @@
#include <cstring> #include <cstring>
#include "ble_uuid.h" #include "ble_uuid.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace esp32_ble { namespace esp32_ble {
@ -143,7 +144,7 @@ void BLEAdvertising::loop() {
if (this->raw_advertisements_callbacks_.empty()) { if (this->raw_advertisements_callbacks_.empty()) {
return; return;
} }
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
this->stop(); this->stop();
this->current_adv_index_ += 1; this->current_adv_index_ += 1;

View File

@ -1,8 +1,8 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import MutableMapping from collections.abc import Callable, MutableMapping
import logging import logging
from typing import Any, Callable from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg

View File

@ -296,7 +296,7 @@ async def to_code(config):
add_idf_component( add_idf_component(
name="esp32-camera", name="esp32-camera",
repo="https://github.com/espressif/esp32-camera.git", repo="https://github.com/espressif/esp32-camera.git",
ref="v2.0.9", ref="v2.0.15",
) )
for conf in config.get(CONF_ON_STREAM_START, []): for conf in config.get(CONF_ON_STREAM_START, []):

View File

@ -3,6 +3,7 @@
#include "esp32_camera.h" #include "esp32_camera.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include <freertos/task.h> #include <freertos/task.h>
@ -54,11 +55,7 @@ void ESP32Camera::dump_config() {
ESP_LOGCONFIG(TAG, " HREF Pin: %d", conf.pin_href); ESP_LOGCONFIG(TAG, " HREF Pin: %d", conf.pin_href);
ESP_LOGCONFIG(TAG, " Pixel Clock Pin: %d", conf.pin_pclk); ESP_LOGCONFIG(TAG, " Pixel Clock Pin: %d", conf.pin_pclk);
ESP_LOGCONFIG(TAG, " External Clock: Pin:%d Frequency:%u", conf.pin_xclk, conf.xclk_freq_hz); ESP_LOGCONFIG(TAG, " External Clock: Pin:%d Frequency:%u", conf.pin_xclk, conf.xclk_freq_hz);
#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated
ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sscb_sda, conf.pin_sscb_scl);
#else
ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sccb_sda, conf.pin_sccb_scl); ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sccb_sda, conf.pin_sccb_scl);
#endif
ESP_LOGCONFIG(TAG, " Reset Pin: %d", conf.pin_reset); ESP_LOGCONFIG(TAG, " Reset Pin: %d", conf.pin_reset);
switch (this->config_.frame_size) { switch (this->config_.frame_size) {
case FRAMESIZE_QQVGA: case FRAMESIZE_QQVGA:
@ -162,7 +159,7 @@ void ESP32Camera::loop() {
} }
// request idle image every idle_update_interval // request idle image every idle_update_interval
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
this->last_idle_request_ = now; this->last_idle_request_ = now;
this->request_image(IDLE); this->request_image(IDLE);
@ -238,13 +235,8 @@ void ESP32Camera::set_external_clock(uint8_t pin, uint32_t frequency) {
this->config_.xclk_freq_hz = frequency; this->config_.xclk_freq_hz = frequency;
} }
void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) { void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) {
#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated
this->config_.pin_sscb_sda = sda;
this->config_.pin_sscb_scl = scl;
#else
this->config_.pin_sccb_sda = sda; this->config_.pin_sccb_sda = sda;
this->config_.pin_sccb_scl = scl; this->config_.pin_sccb_scl = scl;
#endif
} }
void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; } void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; }
void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; }

View File

@ -92,7 +92,7 @@ void ESP32ImprovComponent::loop() {
if (!this->incoming_data_.empty()) if (!this->incoming_data_.empty())
this->process_incoming_data_(); this->process_incoming_data_();
uint32_t now = millis(); uint32_t now = App.get_loop_component_start_time();
switch (this->state_) { switch (this->state_) {
case improv::STATE_STOPPED: case improv::STATE_STOPPED:

View File

@ -288,7 +288,7 @@ uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) {
} }
void ESP32TouchComponent::loop() { void ESP32TouchComponent::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250;
for (auto *child : this->children_) { for (auto *child : this->children_) {
child->value_ = this->component_touch_pad_read(child->get_touch_pad()); child->value_ = this->component_touch_pad_read(child->get_touch_pad());

View File

@ -111,6 +111,8 @@ void ESPHomeOTAComponent::handle_() {
int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno); ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno);
client_->close();
client_ = nullptr;
return; return;
} }

View File

@ -240,7 +240,7 @@ void EthernetComponent::setup() {
} }
void EthernetComponent::loop() { void EthernetComponent::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) { switch (this->state_) {
case EthernetComponentState::STOPPED: case EthernetComponentState::STOPPED:

View File

@ -1,6 +1,7 @@
#include "feedback_cover.h" #include "feedback_cover.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace feedback { namespace feedback {
@ -220,7 +221,7 @@ void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_o
void FeedbackCover::loop() { void FeedbackCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE) if (this->current_operation == COVER_OPERATION_IDLE)
return; return;
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
// Recompute position every loop cycle // Recompute position every loop cycle
this->recompute_position_(); this->recompute_position_();

View File

@ -8,7 +8,7 @@ FujitsuGeneralClimate = fujitsu_general_ns.class_(
"FujitsuGeneralClimate", climate_ir.ClimateIR "FujitsuGeneralClimate", climate_ir.ClimateIR
) )
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(FujitsuGeneralClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(FujitsuGeneralClimate)
async def to_code(config): async def to_code(config):

View File

@ -6,6 +6,7 @@
*/ */
#include "gcja5.h" #include "gcja5.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cstring> #include <cstring>
namespace esphome { namespace esphome {
@ -16,7 +17,7 @@ static const char *const TAG = "gcja5";
void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); } void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); }
void GCJA5Component::loop() { void GCJA5Component::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) { if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index. // last transmission too long ago. Reset RX index.
this->rx_message_.clear(); this->rx_message_.clear();

View File

@ -21,7 +21,7 @@ MODELS = {
"yag": Model.GREE_YAG, "yag": Model.GREE_YAG,
} }
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(GreeClimate).extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
{ {
cv.Required(CONF_MODEL): cv.enum(MODELS), cv.Required(CONF_MODEL): cv.enum(MODELS),
} }

View File

@ -1,5 +1,6 @@
#include "growatt_solar.h" #include "growatt_solar.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace growatt_solar { namespace growatt_solar {
@ -18,7 +19,7 @@ void GrowattSolar::loop() {
void GrowattSolar::update() { void GrowattSolar::update() {
// If our last send has had no reply yet, and it wasn't that long ago, do nothing. // If our last send has had no reply yet, and it wasn't that long ago, do nothing.
uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_send_ < this->get_update_interval() / 2) { if (now - this->last_send_ < this->get_update_interval() / 2) {
return; return;
} }

View File

@ -97,7 +97,7 @@ VERTICAL_DIRECTIONS = {
} }
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
climate_ir.climare_ir_with_receiver_schema(HeatpumpIRClimate).extend( climate_ir.climate_ir_with_receiver_schema(HeatpumpIRClimate).extend(
{ {
cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS), cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS),
cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS), cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS),

View File

@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
hitachi_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344") hitachi_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344")
HitachiClimate = hitachi_ac344_ns.class_("HitachiClimate", climate_ir.ClimateIR) HitachiClimate = hitachi_ac344_ns.class_("HitachiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate)
async def to_code(config): async def to_code(config):

View File

@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424") hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424")
HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR) HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate)
async def to_code(config): async def to_code(config):

View File

@ -1,5 +1,6 @@
#include "kuntze.h" #include "kuntze.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace kuntze { namespace kuntze {
@ -60,7 +61,7 @@ void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
} }
void Kuntze::loop() { void Kuntze::loop() {
uint32_t now = millis(); uint32_t now = App.get_loop_component_start_time();
// timeout after 15 seconds // timeout after 15 seconds
if (this->waiting_ && (now - this->last_send_ > 15000)) { if (this->waiting_ && (now - this->last_send_ > 15000)) {
ESP_LOGW(TAG, "timed out waiting for response"); ESP_LOGW(TAG, "timed out waiting for response");

View File

@ -1,5 +1,5 @@
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable
import esphome.codegen as cg import esphome.codegen as cg

View File

@ -10,6 +10,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
@ -89,6 +90,7 @@ UART_SELECTION_ESP32 = {
VARIANT_ESP32C2: [UART0, UART1], VARIANT_ESP32C2: [UART0, UART1],
VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32P4: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
} }
UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1]
@ -206,6 +208,7 @@ CONFIG_SCHEMA = cv.All(
esp32_c3_idf=USB_SERIAL_JTAG, esp32_c3_idf=USB_SERIAL_JTAG,
esp32_c6_arduino=USB_CDC, esp32_c6_arduino=USB_CDC,
esp32_c6_idf=USB_SERIAL_JTAG, esp32_c6_idf=USB_SERIAL_JTAG,
esp32_p4_idf=USB_SERIAL_JTAG,
rp2040=USB_CDC, rp2040=USB_CDC,
bk72xx=DEFAULT, bk72xx=DEFAULT,
rtl87xx=DEFAULT, rtl87xx=DEFAULT,

View File

@ -16,9 +16,14 @@ static const char *const TAG = "logger";
#ifdef USE_ESP32 #ifdef USE_ESP32
// Implementation for ESP32 (multi-task platform with task-specific tracking) // Implementation for ESP32 (multi-task platform with task-specific tracking)
// Main task always uses direct buffer access for console output and callbacks // Main task always uses direct buffer access for console output and callbacks
// Other tasks: //
// - With task log buffer: stack buffer for console output, async buffer for callbacks // For non-main tasks:
// - Without task log buffer: only console output, no callbacks // - WITH task log buffer: Prefer sending to ring buffer for async processing
// - Avoids allocating stack memory for console output in normal operation
// - Prevents console corruption from concurrent writes by multiple tasks
// - Messages are serialized through main loop for proper console output
// - Fallback to emergency console logging only if ring buffer is full
// - WITHOUT task log buffer: Only emergency console output, no callbacks
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag)) if (level > this->level_for(tag))
return; return;
@ -38,8 +43,18 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *
return; return;
} }
// For non-main tasks: use stack-allocated buffer only for console output bool message_sent = false;
if (this->baud_rate_ > 0) { // If logging is enabled, write to console #ifdef USE_ESPHOME_TASK_LOG_BUFFER
// For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered
message_sent = this->log_buffer_->send_message_thread_safe(static_cast<uint8_t>(level), tag,
static_cast<uint16_t>(line), current_task, format, args);
#endif // USE_ESPHOME_TASK_LOG_BUFFER
// Emergency console logging for non-main tasks when ring buffer is full or disabled
// This is a fallback mechanism to ensure critical log messages are visible
// Note: This may cause interleaved/corrupted console output if multiple tasks
// log simultaneously, but it's better than losing important messages entirely
if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console
// Maximum size for console log messages (includes null terminator) // Maximum size for console log messages (includes null terminator)
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
@ -49,15 +64,6 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *
this->write_msg_(console_buffer); this->write_msg_(console_buffer);
} }
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
// For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered
if (this->log_callback_.size() > 0) {
// This will be processed in the main loop
this->log_buffer_->send_message_thread_safe(static_cast<uint8_t>(level), tag, static_cast<uint16_t>(line),
current_task, format, args);
}
#endif // USE_ESPHOME_TASK_LOG_BUFFER
// Reset the recursion guard for this task // Reset the recursion guard for this task
this->reset_task_log_recursion_(is_main_task); this->reset_task_log_recursion_(is_main_task);
} }
@ -184,7 +190,17 @@ void Logger::loop() {
this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
this->tx_buffer_[this->tx_buffer_at_] = '\0'; this->tx_buffer_[this->tx_buffer_at_] = '\0';
this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_); this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_);
// At this point all the data we need from message has been transferred to the tx_buffer
// so we can release the message to allow other tasks to use it as soon as possible.
this->log_buffer_->release_message_main_loop(received_token); this->log_buffer_->release_message_main_loop(received_token);
// Write to console from the main loop to prevent corruption from concurrent writes
// This ensures all log messages appear on the console in a clean, serialized manner
// Note: Messages may appear slightly out of order due to async processing, but
// this is preferred over corrupted/interleaved console output
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_);
}
} }
} }
#endif #endif

View File

@ -18,12 +18,12 @@
#endif #endif
#endif #endif
#include "freertos/FreeRTOS.h"
#include "esp_idf_version.h" #include "esp_idf_version.h"
#include "freertos/FreeRTOS.h"
#include <fcntl.h>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <fcntl.h>
#endif // USE_ESP_IDF #endif // USE_ESP_IDF
@ -174,11 +174,11 @@ void Logger::pre_setup() {
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
void HOT Logger::write_msg_(const char *msg) { void HOT Logger::write_msg_(const char *msg) {
if ( if (
#if defined(USE_ESP32_VARIANT_ESP32S2) #if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC this->uart_ == UART_SELECTION_USB_CDC
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) #elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC)
this->uart_ == UART_SELECTION_USB_SERIAL_JTAG this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
#elif defined(USE_ESP32_VARIANT_ESP32S3) #elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
#else #else
/* DISABLES CODE */ (false) // NOLINT /* DISABLES CODE */ (false) // NOLINT

View File

@ -321,7 +321,7 @@ async def to_code(configs):
frac = 2 frac = 2
elif frac > 0.19: elif frac > 0.19:
frac = 4 frac = 4
else: elif frac != 0:
frac = 8 frac = 8
displays = [ displays = [
await cg.get_variable(display) for display in config[df.CONF_DISPLAYS] await cg.get_variable(display) for display in config[df.CONF_DISPLAYS]
@ -422,7 +422,7 @@ LVGL_SCHEMA = cv.All(
): lvalid.lv_font, ): lvalid.lv_font,
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean, cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int, cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage, cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of( cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of(
*df.LV_LOG_LEVELS, upper=True *df.LV_LOG_LEVELS, upper=True
), ),

View File

@ -1,4 +1,5 @@
from typing import Any, Callable from collections.abc import Callable
from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg

View File

@ -1,5 +1,3 @@
from typing import Union
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import image from esphome.components import image
from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
@ -361,7 +359,7 @@ lv_image_list = LValidator(
lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal)
def lv_pct(value: Union[int, float]): def lv_pct(value: int | float):
if isinstance(value, float): if isinstance(value, float):
value = int(value * 100) value = int(value * 100)
return literal(f"lv_pct({value})") return literal(f"lv_pct({value})")

View File

@ -1,5 +1,4 @@
import abc import abc
from typing import Union
from esphome import codegen as cg from esphome import codegen as cg
from esphome.config import Config from esphome.config import Config
@ -75,7 +74,7 @@ class CodeContext(abc.ABC):
code_context = None code_context = None
@abc.abstractmethod @abc.abstractmethod
def add(self, expression: Union[Expression, Statement]): def add(self, expression: Expression | Statement):
pass pass
@staticmethod @staticmethod
@ -89,13 +88,13 @@ class CodeContext(abc.ABC):
CodeContext.append(RawStatement("}")) CodeContext.append(RawStatement("}"))
@staticmethod @staticmethod
def append(expression: Union[Expression, Statement]): def append(expression: Expression | Statement):
if CodeContext.code_context is not None: if CodeContext.code_context is not None:
CodeContext.code_context.add(expression) CodeContext.code_context.add(expression)
return expression return expression
def __init__(self): def __init__(self):
self.previous: Union[CodeContext | None] = None self.previous: CodeContext | None = None
self.indent_level = 0 self.indent_level = 0
async def __aenter__(self): async def __aenter__(self):
@ -121,7 +120,7 @@ class MainContext(CodeContext):
Code generation into the main() function Code generation into the main() function
""" """
def add(self, expression: Union[Expression, Statement]): def add(self, expression: Expression | Statement):
return cg.add(self.indented_statement(expression)) return cg.add(self.indented_statement(expression))
@ -144,7 +143,7 @@ class LambdaContext(CodeContext):
self.capture = capture self.capture = capture
self.where = where self.where = where
def add(self, expression: Union[Expression, Statement]): def add(self, expression: Expression | Statement):
self.code_list.append(self.indented_statement(expression)) self.code_list.append(self.indented_statement(expression))
return expression return expression
@ -186,7 +185,7 @@ class LvContext(LambdaContext):
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
await super().__aexit__(exc_type, exc_val, exc_tb) await super().__aexit__(exc_type, exc_val, exc_tb)
def add(self, expression: Union[Expression, Statement]): def add(self, expression: Expression | Statement):
cg.add(expression) cg.add(expression)
return expression return expression
@ -303,7 +302,7 @@ lvgl_static = MockObj("LvglComponent", "::")
# equivalent to cg.add() for the current code context # equivalent to cg.add() for the current code context
def lv_add(expression: Union[Expression, Statement]): def lv_add(expression: Expression | Statement):
return CodeContext.append(expression) return CodeContext.append(expression)

View File

@ -11,6 +11,8 @@ namespace esphome {
namespace lvgl { namespace lvgl {
static const char *const TAG = "lvgl"; static const char *const TAG = "lvgl";
static const size_t MIN_BUFFER_FRAC = 8;
static const char *const EVENT_NAMES[] = { static const char *const EVENT_NAMES[] = {
"NONE", "NONE",
"PRESSED", "PRESSED",
@ -85,6 +87,7 @@ lv_event_code_t lv_update_event; // NOLINT
void LvglComponent::dump_config() { void LvglComponent::dump_config() {
ESP_LOGCONFIG(TAG, "LVGL:"); ESP_LOGCONFIG(TAG, "LVGL:");
ESP_LOGCONFIG(TAG, " Display width/height: %d x %d", this->disp_drv_.hor_res, this->disp_drv_.ver_res); ESP_LOGCONFIG(TAG, " Display width/height: %d x %d", this->disp_drv_.hor_res, this->disp_drv_.ver_res);
ESP_LOGCONFIG(TAG, " Buffer size: %zu%%", 100 / this->buffer_frac_);
ESP_LOGCONFIG(TAG, " Rotation: %d", this->rotation); ESP_LOGCONFIG(TAG, " Rotation: %d", this->rotation);
ESP_LOGCONFIG(TAG, " Draw rounding: %d", (int) this->draw_rounding); ESP_LOGCONFIG(TAG, " Draw rounding: %d", (int) this->draw_rounding);
} }
@ -432,18 +435,28 @@ void LvglComponent::setup() {
auto *display = this->displays_[0]; auto *display = this->displays_[0];
auto width = display->get_width(); auto width = display->get_width();
auto height = display->get_height(); auto height = display->get_height();
size_t buffer_pixels = width * height / this->buffer_frac_; auto frac = this->buffer_frac_;
if (frac == 0)
frac = 1;
size_t buffer_pixels = width * height / frac;
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
void *buffer = nullptr; void *buffer = nullptr;
if (this->buffer_frac_ >= 4) if (this->buffer_frac_ >= MIN_BUFFER_FRAC / 2)
buffer = malloc(buf_bytes); // NOLINT buffer = malloc(buf_bytes); // NOLINT
if (buffer == nullptr) if (buffer == nullptr)
buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
// if specific buffer size not set and can't get 100%, try for a smaller one
if (buffer == nullptr && this->buffer_frac_ == 0) {
frac = MIN_BUFFER_FRAC;
buffer_pixels /= MIN_BUFFER_FRAC;
buffer = lv_custom_mem_alloc(buf_bytes / MIN_BUFFER_FRAC); // NOLINT
}
if (buffer == nullptr) { if (buffer == nullptr) {
this->mark_failed();
this->status_set_error("Memory allocation failure"); this->status_set_error("Memory allocation failure");
this->mark_failed();
return; return;
} }
this->buffer_frac_ = frac;
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels); lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels);
this->disp_drv_.hor_res = width; this->disp_drv_.hor_res = width;
this->disp_drv_.ver_res = height; this->disp_drv_.ver_res = height;
@ -453,8 +466,8 @@ void LvglComponent::setup() {
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes)); // NOLINT this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes)); // NOLINT
if (this->rotate_buf_ == nullptr) { if (this->rotate_buf_ == nullptr) {
this->mark_failed();
this->status_set_error("Memory allocation failure"); this->status_set_error("Memory allocation failure");
this->mark_failed();
return; return;
} }
} }

View File

@ -36,29 +36,43 @@ from .types import (
# this will be populated later, in __init__.py to avoid circular imports. # this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {} WIDGET_TYPES: dict = {}
TIME_TEXT_SCHEMA = cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
}
)
PRINTF_TEXT_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
},
),
validate_printf,
)
def _validate_text(value):
"""
Do some sanity checking of the format to get better error messages
than using cv.Any
"""
if value is None:
raise cv.Invalid("No text specified")
if isinstance(value, dict):
if CONF_TIME_FORMAT in value:
return TIME_TEXT_SCHEMA(value)
return PRINTF_TEXT_SCHEMA(value)
return cv.templatable(cv.string)(value)
# A schema for text properties # A schema for text properties
TEXT_SCHEMA = cv.Schema( TEXT_SCHEMA = cv.Schema(
{ {
cv.Optional(CONF_TEXT): cv.Any( cv.Optional(CONF_TEXT): _validate_text,
cv.All(
cv.Schema(
{
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(
cv.lambda_
),
},
),
validate_printf,
),
cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
}
),
cv.templatable(cv.string),
)
} }
) )
@ -247,11 +261,13 @@ FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of)
def part_schema(parts): def part_schema(parts):
""" """
Generate a schema for the various parts (e.g. main:, indicator:) of a widget type Generate a schema for the various parts (e.g. main:, indicator:) of a widget type
:param parts: The parts to include in the schema :param parts: The parts to include
:return: The schema :return: The schema
""" """
return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend( return (
STATE_SCHEMA cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts})
.extend(STATE_SCHEMA)
.extend(FLAG_SCHEMA)
) )
@ -288,22 +304,18 @@ def base_update_schema(widget_type, parts):
:param parts: The allowable parts to specify :param parts: The allowable parts to specify
:return: :return:
""" """
return ( return part_schema(parts).extend(
part_schema(parts) {
.extend( cv.Required(CONF_ID): cv.ensure_list(
{ cv.maybe_simple_value(
cv.Required(CONF_ID): cv.ensure_list( {
cv.maybe_simple_value( cv.Required(CONF_ID): cv.use_id(widget_type),
{ },
cv.Required(CONF_ID): cv.use_id(widget_type), key=CONF_ID,
}, )
key=CONF_ID, ),
) cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
), }
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
}
)
.extend(FLAG_SCHEMA)
) )
@ -321,7 +333,6 @@ def obj_schema(widget_type: WidgetType):
""" """
return ( return (
part_schema(widget_type.parts) part_schema(widget_type.parts)
.extend(FLAG_SCHEMA)
.extend(LAYOUT_SCHEMA) .extend(LAYOUT_SCHEMA)
.extend(ALIGN_TO_SCHEMA) .extend(ALIGN_TO_SCHEMA)
.extend(automation_schema(widget_type.w_type)) .extend(automation_schema(widget_type.w_type))

View File

@ -1,5 +1,5 @@
import sys import sys
from typing import Any, Union from typing import Any
from esphome import codegen as cg, config_validation as cv from esphome import codegen as cg, config_validation as cv
from esphome.config_validation import Invalid from esphome.config_validation import Invalid
@ -262,7 +262,7 @@ async def wait_for_widgets():
await FakeAwaitable(widgets_wait_generator()) await FakeAwaitable(widgets_wait_generator())
async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]: async def get_widgets(config: dict | list, id: str = CONF_ID) -> list[Widget]:
if not config: if not config:
return [] return []
if not isinstance(config, list): if not isinstance(config, list):

View File

@ -24,6 +24,7 @@ from .obj import obj_spec
CONF_TABVIEW = "tabview" CONF_TABVIEW = "tabview"
CONF_TAB_STYLE = "tab_style" CONF_TAB_STYLE = "tab_style"
CONF_CONTENT_STYLE = "content_style"
lv_tab_t = LvType("lv_obj_t") lv_tab_t = LvType("lv_obj_t")
@ -39,6 +40,7 @@ TABVIEW_SCHEMA = cv.Schema(
) )
), ),
cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec.parts), cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec.parts),
cv.Optional(CONF_CONTENT_STYLE): part_schema(obj_spec.parts),
cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of, cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of,
cv.Optional(CONF_SIZE, default="10%"): size, cv.Optional(CONF_SIZE, default="10%"): size,
} }
@ -79,6 +81,11 @@ class TabviewType(WidgetType):
"tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj) "tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj)
) as btnmatrix_obj: ) as btnmatrix_obj:
await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style) await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style)
if content_style := config.get(CONF_CONTENT_STYLE):
with LocalVariable(
"tabview_content", lv_obj_t, rhs=lv_expr.tabview_get_content(w.obj)
) as content_obj:
await set_obj_properties(Widget(content_obj, obj_spec), content_style)
def obj_creator(self, parent: MockObjClass, config: dict): def obj_creator(self, parent: MockObjClass, config: dict):
return lv_expr.call( return lv_expr.call(

View File

@ -1,5 +1,6 @@
#include "matrix_keypad.h" #include "matrix_keypad.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace matrix_keypad { namespace matrix_keypad {
@ -28,7 +29,7 @@ void MatrixKeypad::setup() {
void MatrixKeypad::loop() { void MatrixKeypad::loop() {
static uint32_t active_start = 0; static uint32_t active_start = 0;
static int active_key = -1; static int active_key = -1;
uint32_t now = millis(); uint32_t now = App.get_loop_component_start_time();
int key = -1; int key = -1;
bool error = false; bool error = false;
int pos = 0, row, col; int pos = 0, row, col;

View File

@ -2,6 +2,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "max7219font.h" #include "max7219font.h"
#include <algorithm> #include <algorithm>
@ -63,7 +64,7 @@ void MAX7219Component::dump_config() {
} }
void MAX7219Component::loop() { void MAX7219Component::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
const uint32_t millis_since_last_scroll = now - this->last_scroll_; const uint32_t millis_since_last_scroll = now - this->last_scroll_;
const size_t first_line_size = this->max_displaybuffer_[0].size(); const size_t first_line_size = this->max_displaybuffer_[0].size();
// check if the buffer has shrunk past the current position since last update // check if the buffer has shrunk past the current position since last update

View File

@ -147,7 +147,11 @@ bool StreamingModel::perform_streaming_inference(const int8_t features[PREPROCES
this->recent_streaming_probabilities_[this->last_n_index_] = output->data.uint8[0]; // probability; this->recent_streaming_probabilities_[this->last_n_index_] = output->data.uint8[0]; // probability;
this->unprocessed_probability_status_ = true; this->unprocessed_probability_status_ = true;
} }
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0); if (this->recent_streaming_probabilities_[this->last_n_index_] < this->probability_cutoff_) {
// Only increment ignore windows if less than the probability cutoff; this forces the model to "cool-off" from a
// previous detection and calling ``reset_probabilities`` so it avoids duplicate detections
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
}
} }
return true; return true;
} }

View File

@ -10,7 +10,7 @@ midea_ir_ns = cg.esphome_ns.namespace("midea_ir")
MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR) MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MideaIR).extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend(
{ {
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
} }

View File

@ -43,7 +43,7 @@ VERTICAL_DIRECTIONS = {
} }
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MitsubishiClimate).extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MitsubishiClimate).extend(
{ {
cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE), cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE),
cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean, cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean,

View File

@ -1,6 +1,7 @@
#include "modbus.h" #include "modbus.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace modbus { namespace modbus {
@ -13,7 +14,7 @@ void Modbus::setup() {
} }
} }
void Modbus::loop() { void Modbus::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
while (this->available()) { while (this->available()) {
uint8_t byte; uint8_t byte;

View File

@ -345,7 +345,7 @@ void MQTTClientComponent::loop() {
this->disconnect_reason_.reset(); this->disconnect_reason_.reset();
} }
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) { switch (this->state_) {
case MQTT_CLIENT_DISABLED: case MQTT_CLIENT_DISABLED:

View File

@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
noblex_ns = cg.esphome_ns.namespace("noblex") noblex_ns = cg.esphome_ns.namespace("noblex")
NoblexClimate = noblex_ns.class_("NoblexClimate", climate_ir.ClimateIR) NoblexClimate = noblex_ns.class_("NoblexClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(NoblexClimate) CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(NoblexClimate)
async def to_code(config): async def to_code(config):

View File

@ -21,8 +21,10 @@ from esphome.const import (
CONF_WEB_SERVER, CONF_WEB_SERVER,
DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI, DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CONDUCTIVITY, DEVICE_CLASS_CONDUCTIVITY,
@ -33,6 +35,7 @@ from esphome.const import (
DEVICE_CLASS_DURATION, DEVICE_CLASS_DURATION,
DEVICE_CLASS_EMPTY, DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_ENERGY_DISTANCE,
DEVICE_CLASS_ENERGY_STORAGE, DEVICE_CLASS_ENERGY_STORAGE,
DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS, DEVICE_CLASS_GAS,
@ -54,6 +57,7 @@ from esphome.const import (
DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION,
DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRECIPITATION_INTENSITY,
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_REACTIVE_ENERGY,
DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_SOUND_PRESSURE, DEVICE_CLASS_SOUND_PRESSURE,
@ -68,6 +72,7 @@ from esphome.const import (
DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_VOLUME_STORAGE,
DEVICE_CLASS_WATER, DEVICE_CLASS_WATER,
DEVICE_CLASS_WEIGHT, DEVICE_CLASS_WEIGHT,
DEVICE_CLASS_WIND_DIRECTION,
DEVICE_CLASS_WIND_SPEED, DEVICE_CLASS_WIND_SPEED,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
@ -78,8 +83,10 @@ CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [ DEVICE_CLASSES = [
DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI, DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CONDUCTIVITY, DEVICE_CLASS_CONDUCTIVITY,
@ -90,6 +97,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_DURATION, DEVICE_CLASS_DURATION,
DEVICE_CLASS_EMPTY, DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_ENERGY_DISTANCE,
DEVICE_CLASS_ENERGY_STORAGE, DEVICE_CLASS_ENERGY_STORAGE,
DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS, DEVICE_CLASS_GAS,
@ -111,6 +119,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION,
DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRECIPITATION_INTENSITY,
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_REACTIVE_ENERGY,
DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_SOUND_PRESSURE, DEVICE_CLASS_SOUND_PRESSURE,
@ -125,6 +134,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_VOLUME_STORAGE,
DEVICE_CLASS_WATER, DEVICE_CLASS_WATER,
DEVICE_CLASS_WEIGHT, DEVICE_CLASS_WEIGHT,
DEVICE_CLASS_WIND_DIRECTION,
DEVICE_CLASS_WIND_SPEED, DEVICE_CLASS_WIND_SPEED,
] ]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True

View File

@ -75,7 +75,7 @@ class PNGFormat(Format):
def actions(self): def actions(self):
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", "1.0.2") cg.add_library("pngle", "1.1.0")
IMAGE_FORMATS = { IMAGE_FORMATS = {

View File

@ -34,12 +34,32 @@ static void init_callback(pngle_t *pngle, uint32_t w, uint32_t h) {
* @param h The height of the rectangle to draw. * @param h The height of the rectangle to draw.
* @param rgba The color to paint the rectangle in. * @param rgba The color to paint the rectangle in.
*/ */
static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) { static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const uint8_t rgba[4]) {
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
Color color(rgba[0], rgba[1], rgba[2], rgba[3]); Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
decoder->draw(x, y, w, h, color); decoder->draw(x, y, w, h, color);
} }
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) {
{
pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE);
if (!pngle) {
ESP_LOGE(TAG, "Failed to allocate memory for PNGLE engine!");
return;
}
memset(pngle, 0, PNGLE_T_SIZE);
pngle_reset(pngle);
this->pngle_ = pngle;
}
}
PngDecoder::~PngDecoder() {
if (this->pngle_) {
pngle_reset(this->pngle_);
this->allocator_.deallocate(this->pngle_, PNGLE_T_SIZE);
}
}
int PngDecoder::prepare(size_t download_size) { int PngDecoder::prepare(size_t download_size) {
ImageDecoder::prepare(download_size); ImageDecoder::prepare(download_size);
if (!this->pngle_) { if (!this->pngle_) {

View File

@ -1,7 +1,8 @@
#pragma once #pragma once
#include "image_decoder.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "image_decoder.h"
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include <pngle.h> #include <pngle.h>
@ -18,13 +19,14 @@ class PngDecoder : public ImageDecoder {
* *
* @param display The image to decode the stream into. * @param display The image to decode the stream into.
*/ */
PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} PngDecoder(OnlineImage *image);
~PngDecoder() override { pngle_destroy(this->pngle_); } ~PngDecoder() override;
int prepare(size_t download_size) override; int prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override; int HOT decode(uint8_t *buffer, size_t size) override;
protected: protected:
RAMAllocator<pngle_t> allocator_;
pngle_t *pngle_; pngle_t *pngle_;
}; };

View File

@ -1,5 +1,5 @@
from collections.abc import Awaitable from collections.abc import Awaitable, Callable
from typing import Any, Callable, Optional from typing import Any
import esphome.codegen as cg import esphome.codegen as cg
from esphome.const import CONF_ID from esphome.const import CONF_ID
@ -103,7 +103,7 @@ def define_setting_readers(component_type: str, keys: list[str]) -> None:
def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]):
messages: dict[str, tuple[bool, Optional[int]]] = {} messages: dict[str, tuple[bool, int | None]] = {}
for key in keys: for key in keys:
messages[schemas[key].message] = ( messages[schemas[key].message] = (
schemas[key].keep_updated, schemas[key].keep_updated,

View File

@ -2,7 +2,7 @@
# inputs of the OpenTherm component. # inputs of the OpenTherm component.
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Optional, TypeVar from typing import Any, TypeVar
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@ -61,11 +61,11 @@ TSchema = TypeVar("TSchema", bound=EntitySchema)
class SensorSchema(EntitySchema): class SensorSchema(EntitySchema):
accuracy_decimals: int accuracy_decimals: int
state_class: str state_class: str
unit_of_measurement: Optional[str] = None unit_of_measurement: str | None = None
icon: Optional[str] = None icon: str | None = None
device_class: Optional[str] = None device_class: str | None = None
disabled_by_default: bool = False disabled_by_default: bool = False
order: Optional[int] = None order: int | None = None
SENSORS: dict[str, SensorSchema] = { SENSORS: dict[str, SensorSchema] = {
@ -461,9 +461,9 @@ SENSORS: dict[str, SensorSchema] = {
@dataclass @dataclass
class BinarySensorSchema(EntitySchema): class BinarySensorSchema(EntitySchema):
icon: Optional[str] = None icon: str | None = None
device_class: Optional[str] = None device_class: str | None = None
order: Optional[int] = None order: int | None = None
BINARY_SENSORS: dict[str, BinarySensorSchema] = { BINARY_SENSORS: dict[str, BinarySensorSchema] = {
@ -654,7 +654,7 @@ BINARY_SENSORS: dict[str, BinarySensorSchema] = {
@dataclass @dataclass
class SwitchSchema(EntitySchema): class SwitchSchema(EntitySchema):
default_mode: Optional[str] = None default_mode: str | None = None
SWITCHES: dict[str, SwitchSchema] = { SWITCHES: dict[str, SwitchSchema] = {
@ -721,9 +721,9 @@ class InputSchema(EntitySchema):
unit_of_measurement: str unit_of_measurement: str
step: float step: float
range: tuple[int, int] range: tuple[int, int]
icon: Optional[str] = None icon: str | None = None
auto_max_value: Optional[AutoConfigure] = None auto_max_value: AutoConfigure | None = None
auto_min_value: Optional[AutoConfigure] = None auto_min_value: AutoConfigure | None = None
INPUTS: dict[str, InputSchema] = { INPUTS: dict[str, InputSchema] = {
@ -834,7 +834,7 @@ class SettingSchema(EntitySchema):
backing_type: str backing_type: str
validation_schema: cv.Schema validation_schema: cv.Schema
default_value: Any default_value: Any
order: Optional[int] = None order: int | None = None
SETTINGS: dict[str, SettingSchema] = { SETTINGS: dict[str, SettingSchema] = {

View File

@ -1,4 +1,4 @@
from typing import Callable from collections.abc import Callable
from voluptuous import Schema from voluptuous import Schema

View File

@ -1,5 +1,6 @@
#include "pmsx003.h" #include "pmsx003.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace pmsx003 { namespace pmsx003 {
@ -42,7 +43,7 @@ void PMSX003Component::dump_config() {
} }
void PMSX003Component::loop() { void PMSX003Component::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
// If we update less often than it takes the device to stabilise, spin the fan down // If we update less often than it takes the device to stabilise, spin the fan down
// rather than running it constantly. It does take some time to stabilise, so we // rather than running it constantly. It does take some time to stabilise, so we

View File

@ -2,6 +2,7 @@ import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import ( from esphome.components.esp32 import (
CONF_CPU_FREQUENCY,
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES,
VARIANT_ESP32, VARIANT_ESP32,
add_idf_sdkconfig_option, add_idf_sdkconfig_option,
@ -50,18 +51,23 @@ SPIRAM_SPEEDS = {
def validate_psram_mode(config): def validate_psram_mode(config):
if config[CONF_MODE] == TYPE_OCTAL and config[CONF_SPEED] == 120e6: esp32_config = fv.full_config.get()[PLATFORM_ESP32]
esp32_config = fv.full_config.get()[PLATFORM_ESP32] if config[CONF_SPEED] == 120e6:
if ( if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ":
esp32_config[CONF_FRAMEWORK] raise cv.Invalid(
.get(CONF_ADVANCED, {}) "PSRAM 120MHz requires 240MHz CPU frequency (set in esp32 component)"
.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES)
):
_LOGGER.warning(
"120MHz PSRAM in octal mode is an experimental feature - use at your own risk"
) )
else: if config[CONF_MODE] == TYPE_OCTAL:
raise cv.Invalid("PSRAM 120MHz is not supported in octal mode") if (
esp32_config[CONF_FRAMEWORK]
.get(CONF_ADVANCED, {})
.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES)
):
_LOGGER.warning(
"120MHz PSRAM in octal mode is an experimental feature - use at your own risk"
)
else:
raise cv.Invalid("PSRAM 120MHz is not supported in octal mode")
if config[CONF_MODE] != TYPE_OCTAL and config[CONF_ENABLE_ECC]: if config[CONF_MODE] != TYPE_OCTAL and config[CONF_ENABLE_ECC]:
raise cv.Invalid("ECC is only available in octal mode.") raise cv.Invalid("ECC is only available in octal mode.")
if config[CONF_MODE] == TYPE_OCTAL: if config[CONF_MODE] == TYPE_OCTAL:
@ -112,7 +118,7 @@ async def to_code(config):
add_idf_sdkconfig_option(f"{SPIRAM_MODES[config[CONF_MODE]]}", True) add_idf_sdkconfig_option(f"{SPIRAM_MODES[config[CONF_MODE]]}", True)
add_idf_sdkconfig_option(f"{SPIRAM_SPEEDS[config[CONF_SPEED]]}", True) add_idf_sdkconfig_option(f"{SPIRAM_SPEEDS[config[CONF_SPEED]]}", True)
if config[CONF_MODE] == TYPE_OCTAL and config[CONF_SPEED] == 120e6: if config[CONF_MODE] == TYPE_OCTAL and config[CONF_SPEED] == 120e6:
add_idf_sdkconfig_option("CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240", True) add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True)
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0): if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0):
add_idf_sdkconfig_option( add_idf_sdkconfig_option(
"CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True "CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True

View File

@ -1,5 +1,6 @@
#include "pzem004t.h" #include "pzem004t.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cinttypes> #include <cinttypes>
namespace esphome { namespace esphome {
@ -16,7 +17,7 @@ void PZEM004T::setup() {
} }
void PZEM004T::loop() { void PZEM004T::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_read_ > 500 && this->available() < 7) { if (now - this->last_read_ > 500 && this->available() < 7) {
while (this->available()) while (this->available())
this->read(); this->read();

View File

@ -1,5 +1,6 @@
#include "rf_bridge.h" #include "rf_bridge.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cinttypes> #include <cinttypes>
#include <cstring> #include <cstring>
@ -128,7 +129,7 @@ void RFBridgeComponent::write_byte_str_(const std::string &codes) {
} }
void RFBridgeComponent::loop() { void RFBridgeComponent::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_bridge_byte_ > 50) { if (now - this->last_bridge_byte_ > 50) {
this->rx_buffer_.clear(); this->rx_buffer_.clear();
this->last_bridge_byte_ = now; this->last_bridge_byte_ = now;

View File

@ -1,5 +1,6 @@
#include "sds011.h" #include "sds011.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace sds011 { namespace sds011 {
@ -75,7 +76,7 @@ void SDS011Component::dump_config() {
} }
void SDS011Component::loop() { void SDS011Component::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if ((now - this->last_transmission_ >= 500) && this->data_index_) { if ((now - this->last_transmission_ >= 500) && this->data_index_) {
// last transmission too long ago. Reset RX index. // last transmission too long ago. Reset RX index.
ESP_LOGV(TAG, "Last transmission too long ago. Reset RX index."); ESP_LOGV(TAG, "Last transmission too long ago. Reset RX index.");

View File

@ -25,6 +25,10 @@ static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2;
static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181; static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181;
static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0; static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; // used for VOC and NOx index values
static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
void SEN5XComponent::setup() { void SEN5XComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up sen5x..."); ESP_LOGCONFIG(TAG, "Setting up sen5x...");
@ -88,8 +92,9 @@ void SEN5XComponent::setup() {
product_name_.push_back(current_char); product_name_.push_back(current_char);
// second char // second char
current_char = *current_int & 0xFF; current_char = *current_int & 0xFF;
if (current_char) if (current_char) {
product_name_.push_back(current_char); product_name_.push_back(current_char);
}
} }
current_int++; current_int++;
} while (current_char && --max); } while (current_char && --max);
@ -271,10 +276,10 @@ void SEN5XComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode"); ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode");
break; break;
case MEDIUM_ACCELERATION: case MEDIUM_ACCELERATION:
ESP_LOGCONFIG(TAG, " Medium RH/T accelertion mode"); ESP_LOGCONFIG(TAG, " Medium RH/T acceleration mode");
break; break;
case HIGH_ACCELERATION: case HIGH_ACCELERATION:
ESP_LOGCONFIG(TAG, " High RH/T accelertion mode"); ESP_LOGCONFIG(TAG, " High RH/T acceleration mode");
break; break;
} }
} }
@ -337,47 +342,61 @@ void SEN5XComponent::update() {
ESP_LOGD(TAG, "read data error (%d)", this->last_error_); ESP_LOGD(TAG, "read data error (%d)", this->last_error_);
return; return;
} }
float pm_1_0 = measurements[0] / 10.0;
if (measurements[0] == 0xFFFF)
pm_1_0 = NAN;
float pm_2_5 = measurements[1] / 10.0;
if (measurements[1] == 0xFFFF)
pm_2_5 = NAN;
float pm_4_0 = measurements[2] / 10.0;
if (measurements[2] == 0xFFFF)
pm_4_0 = NAN;
float pm_10_0 = measurements[3] / 10.0;
if (measurements[3] == 0xFFFF)
pm_10_0 = NAN;
float humidity = measurements[4] / 100.0;
if (measurements[4] == 0xFFFF)
humidity = NAN;
float temperature = (int16_t) measurements[5] / 200.0;
if (measurements[5] == 0xFFFF)
temperature = NAN;
float voc = measurements[6] / 10.0;
if (measurements[6] == 0xFFFF)
voc = NAN;
float nox = measurements[7] / 10.0;
if (measurements[7] == 0xFFFF)
nox = NAN;
if (this->pm_1_0_sensor_ != nullptr) ESP_LOGVV(TAG, "pm_1_0 = 0x%.4x", measurements[0]);
float pm_1_0 = measurements[0] == UINT16_MAX ? NAN : measurements[0] / 10.0f;
ESP_LOGVV(TAG, "pm_2_5 = 0x%.4x", measurements[1]);
float pm_2_5 = measurements[1] == UINT16_MAX ? NAN : measurements[1] / 10.0f;
ESP_LOGVV(TAG, "pm_4_0 = 0x%.4x", measurements[2]);
float pm_4_0 = measurements[2] == UINT16_MAX ? NAN : measurements[2] / 10.0f;
ESP_LOGVV(TAG, "pm_10_0 = 0x%.4x", measurements[3]);
float pm_10_0 = measurements[3] == UINT16_MAX ? NAN : measurements[3] / 10.0f;
ESP_LOGVV(TAG, "humidity = 0x%.4x", measurements[4]);
float humidity = measurements[4] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[4]) / 100.0f;
ESP_LOGVV(TAG, "temperature = 0x%.4x", measurements[5]);
float temperature = measurements[5] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[5]) / 200.0f;
ESP_LOGVV(TAG, "voc = 0x%.4x", measurements[6]);
int16_t voc_idx = static_cast<int16_t>(measurements[6]);
float voc = (voc_idx < SEN5X_MIN_INDEX_VALUE || voc_idx > SEN5X_MAX_INDEX_VALUE)
? NAN
: static_cast<float>(voc_idx) / 10.0f;
ESP_LOGVV(TAG, "nox = 0x%.4x", measurements[7]);
int16_t nox_idx = static_cast<int16_t>(measurements[7]);
float nox = (nox_idx < SEN5X_MIN_INDEX_VALUE || nox_idx > SEN5X_MAX_INDEX_VALUE)
? NAN
: static_cast<float>(nox_idx) / 10.0f;
if (this->pm_1_0_sensor_ != nullptr) {
this->pm_1_0_sensor_->publish_state(pm_1_0); this->pm_1_0_sensor_->publish_state(pm_1_0);
if (this->pm_2_5_sensor_ != nullptr) }
if (this->pm_2_5_sensor_ != nullptr) {
this->pm_2_5_sensor_->publish_state(pm_2_5); this->pm_2_5_sensor_->publish_state(pm_2_5);
if (this->pm_4_0_sensor_ != nullptr) }
if (this->pm_4_0_sensor_ != nullptr) {
this->pm_4_0_sensor_->publish_state(pm_4_0); this->pm_4_0_sensor_->publish_state(pm_4_0);
if (this->pm_10_0_sensor_ != nullptr) }
if (this->pm_10_0_sensor_ != nullptr) {
this->pm_10_0_sensor_->publish_state(pm_10_0); this->pm_10_0_sensor_->publish_state(pm_10_0);
if (this->temperature_sensor_ != nullptr) }
if (this->temperature_sensor_ != nullptr) {
this->temperature_sensor_->publish_state(temperature); this->temperature_sensor_->publish_state(temperature);
if (this->humidity_sensor_ != nullptr) }
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->publish_state(humidity); this->humidity_sensor_->publish_state(humidity);
if (this->voc_sensor_ != nullptr) }
if (this->voc_sensor_ != nullptr) {
this->voc_sensor_->publish_state(voc); this->voc_sensor_->publish_state(voc);
if (this->nox_sensor_ != nullptr) }
if (this->nox_sensor_ != nullptr) {
this->nox_sensor_->publish_state(nox); this->nox_sensor_->publish_state(nox);
}
this->status_clear_warning(); this->status_clear_warning();
}); });
} }

View File

@ -43,8 +43,10 @@ from esphome.const import (
CONF_WINDOW_SIZE, CONF_WINDOW_SIZE,
DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI, DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CONDUCTIVITY, DEVICE_CLASS_CONDUCTIVITY,
@ -56,6 +58,7 @@ from esphome.const import (
DEVICE_CLASS_DURATION, DEVICE_CLASS_DURATION,
DEVICE_CLASS_EMPTY, DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_ENERGY_DISTANCE,
DEVICE_CLASS_ENERGY_STORAGE, DEVICE_CLASS_ENERGY_STORAGE,
DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS, DEVICE_CLASS_GAS,
@ -77,6 +80,7 @@ from esphome.const import (
DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION,
DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRECIPITATION_INTENSITY,
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_REACTIVE_ENERGY,
DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_SOUND_PRESSURE, DEVICE_CLASS_SOUND_PRESSURE,
@ -92,6 +96,7 @@ from esphome.const import (
DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_VOLUME_STORAGE,
DEVICE_CLASS_WATER, DEVICE_CLASS_WATER,
DEVICE_CLASS_WEIGHT, DEVICE_CLASS_WEIGHT,
DEVICE_CLASS_WIND_DIRECTION,
DEVICE_CLASS_WIND_SPEED, DEVICE_CLASS_WIND_SPEED,
ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_CONFIG,
) )
@ -104,8 +109,10 @@ CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [ DEVICE_CLASSES = [
DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI, DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CONDUCTIVITY, DEVICE_CLASS_CONDUCTIVITY,
@ -117,6 +124,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_DURATION, DEVICE_CLASS_DURATION,
DEVICE_CLASS_EMPTY, DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_ENERGY_DISTANCE,
DEVICE_CLASS_ENERGY_STORAGE, DEVICE_CLASS_ENERGY_STORAGE,
DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS, DEVICE_CLASS_GAS,
@ -138,6 +146,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION,
DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRECIPITATION_INTENSITY,
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_REACTIVE_ENERGY,
DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_SOUND_PRESSURE, DEVICE_CLASS_SOUND_PRESSURE,
@ -153,6 +162,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_VOLUME_STORAGE, DEVICE_CLASS_VOLUME_STORAGE,
DEVICE_CLASS_WATER, DEVICE_CLASS_WATER,
DEVICE_CLASS_WEIGHT, DEVICE_CLASS_WEIGHT,
DEVICE_CLASS_WIND_DIRECTION,
DEVICE_CLASS_WIND_SPEED, DEVICE_CLASS_WIND_SPEED,
] ]

Some files were not shown because too many files have changed in this diff Show More