mirror of
https://github.com/esphome/esphome.git
synced 2025-08-02 16:37:46 +00:00
Merge remote-tracking branch 'upstream/dev' into add_api_stats
This commit is contained in:
commit
1c06137ae0
37
.devcontainer/Dockerfile
Normal file
37
.devcontainer/Dockerfile
Normal 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
|
@ -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"
|
||||||
],
|
],
|
||||||
|
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@ -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
|
||||||
|
2
.github/workflows/ci-docker.yml
vendored
2
.github/workflows/ci-docker.yml
vendored
@ -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
|
||||||
|
|
||||||
|
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@ -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 }}
|
||||||
|
|
||||||
|
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@ -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
|
||||||
|
6
.github/workflows/sync-device-classes.yml
vendored
6
.github/workflows/sync-device-classes.yml
vendored
@ -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
1
.gitignore
vendored
@ -143,3 +143,4 @@ sdkconfig.*
|
|||||||
/components
|
/components
|
||||||
/managed_components
|
/managed_components
|
||||||
|
|
||||||
|
api-docs/
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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 /
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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 */
|
||||||
|
@ -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):
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
1
esphome/components/cm1106/__init__.py
Normal file
1
esphome/components/cm1106/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""CM1106 component for ESPHome."""
|
112
esphome/components/cm1106/cm1106.cpp
Normal file
112
esphome/components/cm1106/cm1106.cpp
Normal 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
|
40
esphome/components/cm1106/cm1106.h
Normal file
40
esphome/components/cm1106/cm1106.h
Normal 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
|
72
esphome/components/cm1106/sensor.py
Normal file
72
esphome/components/cm1106/sensor.py
Normal 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)
|
@ -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):
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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.");
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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_{};
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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;
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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")
|
||||||
|
@ -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,
|
||||||
|
43
esphome/components/esp32/gpio_esp32_p4.py
Normal file
43
esphome/components/esp32/gpio_esp32_p4.py
Normal 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
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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, []):
|
||||||
|
@ -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; }
|
||||||
|
@ -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:
|
||||||
|
@ -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());
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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_();
|
||||||
|
@ -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):
|
||||||
|
@ -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();
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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");
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
),
|
),
|
||||||
|
@ -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
|
||||||
|
@ -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})")
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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):
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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 = {
|
||||||
|
@ -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_) {
|
||||||
|
@ -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_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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] = {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
from voluptuous import Schema
|
from voluptuous import Schema
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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.");
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user