Compare commits

..

3 Commits

Author SHA1 Message Date
J. Nick Koston
c37f372885 Merge branch 'dev' into str_sprintf 2026-01-21 19:51:45 -10:00
J. Nick Koston
06c619b2e0 [ci] Soft-deprecate str_sprintf/str_snprintf to prevent hidden heap allocations 2026-01-14 15:48:22 -10:00
J. Nick Koston
71c922bb60 [ci] Soft-deprecate str_sprintf/str_snprintf to prevent hidden heap allocations 2026-01-14 15:46:09 -10:00
96 changed files with 9491 additions and 10733 deletions

View File

@@ -17,7 +17,7 @@ runs:
steps: steps:
- name: Set up Python ${{ inputs.python-version }} - name: Set up Python ${{ inputs.python-version }}
id: python id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment

View File

@@ -22,7 +22,7 @@ jobs:
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token

View File

@@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python - name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "3.11" python-version: "3.11"

View File

@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python - name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "3.11" python-version: "3.11"

View File

@@ -43,9 +43,9 @@ jobs:
- "docker" - "docker"
# - "lint" # - "lint"
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python - name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "3.11" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository - name: Check out code from base repository
if: steps.pr.outputs.skip != 'true' if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
# Always check out from the base repository (esphome/esphome), never from forks # Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo # Use the PR's target branch to ensure we run trusted code from the main repo

View File

@@ -36,13 +36,13 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }} cache-key: ${{ steps.cache-key.outputs.key }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Generate cache-key - name: Generate cache-key
id: cache-key id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true' if: needs.determine-jobs.outputs.python-linters == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -91,7 +91,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -132,7 +132,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python - name: Restore Python
id: restore-python id: restore-python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
@@ -183,7 +183,7 @@ jobs:
component-test-batches: ${{ steps.determine.outputs.component-test-batches }} component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
# Fetch enough history to find the merge base # Fetch enough history to find the merge base
fetch-depth: 2 fetch-depth: 2
@@ -237,10 +237,10 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true' if: needs.determine-jobs.outputs.integration-tests == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python 3.13 - name: Set up Python 3.13
id: python id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "3.13" python-version: "3.13"
- name: Restore Python virtual environment - name: Restore Python virtual environment
@@ -273,7 +273,7 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
@@ -321,7 +321,7 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
# Need history for HEAD~1 to work for checking changed files # Need history for HEAD~1 to work for checking changed files
fetch-depth: 2 fetch-depth: 2
@@ -400,7 +400,7 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
# Need history for HEAD~1 to work for checking changed files # Need history for HEAD~1 to work for checking changed files
fetch-depth: 2 fetch-depth: 2
@@ -489,7 +489,7 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
# Need history for HEAD~1 to work for checking changed files # Need history for HEAD~1 to work for checking changed files
fetch-depth: 2 fetch-depth: 2
@@ -577,7 +577,7 @@ jobs:
version: 1.0 version: 1.0
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -662,7 +662,7 @@ jobs:
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -688,7 +688,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip }} skip: ${{ steps.check-script.outputs.skip }}
steps: steps:
- name: Check out target branch - name: Check out target branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}
@@ -840,7 +840,7 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }} flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps: steps:
- name: Check out PR branch - name: Check out PR branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -908,7 +908,7 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:

View File

@@ -54,11 +54,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }} branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }} deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get tag - name: Get tag
id: tag id: tag
# yamllint disable rule:line-length # yamllint disable rule:line-length
@@ -60,9 +60,9 @@ jobs:
contents: read contents: read
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python - name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Build - name: Build
@@ -92,9 +92,9 @@ jobs:
os: "ubuntu-24.04-arm" os: "ubuntu-24.04-arm"
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python - name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: "3.11" python-version: "3.11"
@@ -168,7 +168,7 @@ jobs:
- ghcr - ghcr
- dockerhub - dockerhub
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download digests - name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0

View File

@@ -13,16 +13,16 @@ jobs:
if: github.repository == 'esphome/esphome' if: github.repository == 'esphome/esphome'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Checkout Home Assistant - name: Checkout Home Assistant
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
repository: home-assistant/core repository: home-assistant/core
path: lib/home-assistant path: lib/home-assistant
- name: Setup Python - name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: 3.13 python-version: 3.13

View File

@@ -11,7 +11,7 @@ ci:
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.14.14 rev: v0.14.13
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -88,8 +88,7 @@ esphome/components/bmp3xx/* @latonita
esphome/components/bmp3xx_base/* @latonita @martgras esphome/components/bmp3xx_base/* @latonita @martgras
esphome/components/bmp3xx_i2c/* @latonita esphome/components/bmp3xx_i2c/* @latonita
esphome/components/bmp3xx_spi/* @latonita esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581_base/* @danielkent-net @kahrendt esphome/components/bmp581/* @kahrendt
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bp1658cj/* @Cossid esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid esphome/components/bp5758d/* @Cossid
esphome/components/bthome_mithermometer/* @nagyrobi esphome/components/bthome_mithermometer/* @nagyrobi
@@ -482,7 +481,6 @@ esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337 esphome/components/sx126x/* @swoboda1337
esphome/components/sx127x/* @swoboda1337 esphome/components/sx127x/* @swoboda1337
esphome/components/sy6970/* @linkedupbits
esphome/components/syslog/* @clydebarrow esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan esphome/components/tc74/* @sethgirvan

View File

@@ -1844,8 +1844,23 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
return false; return false;
} }
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details // Toggle Nagle's algorithm based on message type to prevent log messages from
this->helper_->set_nodelay_for_message(is_log_message); // filling the TCP send buffer and crowding out important state updates.
//
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
// message with `option (no_delay) = false;` in api.proto, indicating it should
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
//
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
// (~200ms), buffer filling, or when a state update triggers a flush.
//
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
// for immediate delivery. These are time-sensitive and should not be delayed.
//
// This must be done proactively BEFORE the buffer fills up - checking buffer
// state here would be too late since we'd already be in a degraded state.
this->helper_->set_nodelay(!is_log_message);
APIError err = this->helper_->write_protobuf_packet(message_type, buffer); APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
if (err == APIError::WOULD_BLOCK) if (err == APIError::WOULD_BLOCK)

View File

@@ -120,39 +120,26 @@ class APIFrameHelper {
} }
return APIError::OK; return APIError::OK;
} }
// Manage TCP_NODELAY (Nagle's algorithm) based on message type. /// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
// ///
// For non-log messages (sensor data, state updates): Always disable Nagle /// This is used to allow log messages to coalesce (Nagle enabled) while keeping
// (NODELAY on) for immediate delivery - these are time-sensitive. /// state updates low-latency (NODELAY enabled). Without this, many small log
// /// packets fill the TCP send buffer, crowding out important state updates.
// For log messages: Use Nagle to coalesce multiple small log packets into ///
// fewer larger packets, reducing WiFi overhead. However, we limit batching /// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained /// this is just a boolean assignment; on other platforms it's a lightweight syscall.
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into ///
// shared pbufs, but holding data too long waiting for Nagle's timer causes /// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
// buffer exhaustion and dropped messages. /// @return true if successful or already in desired state
// bool set_nodelay(bool enable) {
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all) if (this->nodelay_enabled_ == enable)
// return true;
void set_nodelay_for_message(bool is_log_message) { int val = enable ? 1 : 0;
if (!is_log_message) { int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
if (this->nodelay_state_ != NODELAY_ON) { if (err == 0) {
this->set_nodelay_raw_(true); this->nodelay_enabled_ = enable;
this->nodelay_state_ = NODELAY_ON;
}
return;
}
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
if (this->nodelay_state_ == NODELAY_ON) {
this->set_nodelay_raw_(false);
this->nodelay_state_ = 1;
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
} else {
this->nodelay_state_++;
} }
return err == 0;
} }
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf messages in a single operation // Write multiple protobuf messages in a single operation
@@ -242,18 +229,10 @@ class APIFrameHelper {
uint8_t tx_buf_head_{0}; uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0}; uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0}; uint8_t tx_buf_count_{0};
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled // Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
// (immediate send). Values 1-2 count log messages in the current Nagle batch. // since init_common_() enables NODELAY. Used by set_nodelay() to allow log
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset. // messages to coalesce while keeping state updates low-latency.
static constexpr int8_t NODELAY_ON = -1; bool nodelay_enabled_{true};
static constexpr int8_t LOG_NAGLE_COUNT = 2;
int8_t nodelay_state_{NODELAY_ON};
// Internal helper to set TCP_NODELAY socket option
void set_nodelay_raw_(bool enable) {
int val = enable ? 1 : 0;
this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
}
// Common initialization for both plaintext and noise protocols // Common initialization for both plaintext and noise protocols
APIError init_common_(); APIError init_common_();

View File

@@ -13,11 +13,14 @@ from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
CODEOWNERS = ["@jasstrong"] CODEOWNERS = ["@jasstrong"]
DEPENDENCIES = ["sensor"] DEPENDENCIES = ["sensor"]
UNIT_INDEX = "index"
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component) AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
sensor.sensor_schema( sensor.sensor_schema(
AQISensor, AQISensor,
unit_of_measurement=UNIT_INDEX,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI, device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,

View File

@@ -108,14 +108,10 @@ void ATM90E32Component::update() {
#endif #endif
} }
void ATM90E32Component::get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
this->cs_->dump_summary(buffer.data(), buffer.size());
}
void ATM90E32Component::setup() { void ATM90E32Component::setup() {
this->spi_setup(); this->spi_setup();
char cs[GPIO_SUMMARY_MAX_LEN]; this->cs_summary_ = this->cs_->dump_summary();
this->get_cs_summary_(cs); const char *cs = this->cs_summary_.c_str();
uint16_t mmode0 = 0x87; // 3P4W 50Hz uint16_t mmode0 = 0x87; // 3P4W 50Hz
uint16_t high_thresh = 0; uint16_t high_thresh = 0;
@@ -163,13 +159,13 @@ void ATM90E32Component::setup() {
if (this->enable_offset_calibration_) { if (this->enable_offset_calibration_) {
// Initialize flash storage for offset calibrations // Initialize flash storage for offset calibrations
uint32_t o_hash = fnv1_hash("_offset_calibration_"); uint32_t o_hash = fnv1_hash("_offset_calibration_");
o_hash = fnv1_hash_extend(o_hash, cs); o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true); this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
this->restore_offset_calibrations_(); this->restore_offset_calibrations_();
// Initialize flash storage for power offset calibrations // Initialize flash storage for power offset calibrations
uint32_t po_hash = fnv1_hash("_power_offset_calibration_"); uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
po_hash = fnv1_hash_extend(po_hash, cs); po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true); this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
this->restore_power_offset_calibrations_(); this->restore_power_offset_calibrations_();
} else { } else {
@@ -190,7 +186,7 @@ void ATM90E32Component::setup() {
if (this->enable_gain_calibration_) { if (this->enable_gain_calibration_) {
// Initialize flash storage for gain calibration // Initialize flash storage for gain calibration
uint32_t g_hash = fnv1_hash("_gain_calibration_"); uint32_t g_hash = fnv1_hash("_gain_calibration_");
g_hash = fnv1_hash_extend(g_hash, cs); g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true); this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
this->restore_gain_calibrations_(); this->restore_gain_calibrations_();
@@ -221,8 +217,7 @@ void ATM90E32Component::setup() {
} }
void ATM90E32Component::log_calibration_status_() { void ATM90E32Component::log_calibration_status_() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
bool offset_mismatch = false; bool offset_mismatch = false;
bool power_mismatch = false; bool power_mismatch = false;
@@ -573,8 +568,7 @@ float ATM90E32Component::get_chip_temperature_() {
} }
void ATM90E32Component::run_gain_calibrations() { void ATM90E32Component::run_gain_calibrations() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
if (!this->enable_gain_calibration_) { if (!this->enable_gain_calibration_) {
ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true", ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
cs); cs);
@@ -674,8 +668,7 @@ void ATM90E32Component::run_gain_calibrations() {
} }
void ATM90E32Component::save_gain_calibration_to_memory_() { void ATM90E32Component::save_gain_calibration_to_memory_() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
bool success = this->gain_calibration_pref_.save(&this->gain_phase_); bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
global_preferences->sync(); global_preferences->sync();
if (success) { if (success) {
@@ -688,8 +681,7 @@ void ATM90E32Component::save_gain_calibration_to_memory_() {
} }
void ATM90E32Component::save_offset_calibration_to_memory_() { void ATM90E32Component::save_offset_calibration_to_memory_() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
bool success = this->offset_pref_.save(&this->offset_phase_); bool success = this->offset_pref_.save(&this->offset_phase_);
global_preferences->sync(); global_preferences->sync();
if (success) { if (success) {
@@ -705,8 +697,7 @@ void ATM90E32Component::save_offset_calibration_to_memory_() {
} }
void ATM90E32Component::save_power_offset_calibration_to_memory_() { void ATM90E32Component::save_power_offset_calibration_to_memory_() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
bool success = this->power_offset_pref_.save(&this->power_offset_phase_); bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
global_preferences->sync(); global_preferences->sync();
if (success) { if (success) {
@@ -722,8 +713,7 @@ void ATM90E32Component::save_power_offset_calibration_to_memory_() {
} }
void ATM90E32Component::run_offset_calibrations() { void ATM90E32Component::run_offset_calibrations() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
if (!this->enable_offset_calibration_) { if (!this->enable_offset_calibration_) {
ESP_LOGW(TAG, ESP_LOGW(TAG,
"[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true", "[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true",
@@ -753,8 +743,7 @@ void ATM90E32Component::run_offset_calibrations() {
} }
void ATM90E32Component::run_power_offset_calibrations() { void ATM90E32Component::run_power_offset_calibrations() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
if (!this->enable_offset_calibration_) { if (!this->enable_offset_calibration_) {
ESP_LOGW( ESP_LOGW(
TAG, TAG,
@@ -827,8 +816,7 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t
} }
void ATM90E32Component::restore_gain_calibrations_() { void ATM90E32Component::restore_gain_calibrations_() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
for (uint8_t i = 0; i < 3; ++i) { for (uint8_t i = 0; i < 3; ++i) {
this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_; this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_;
this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_; this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_;
@@ -882,8 +870,7 @@ void ATM90E32Component::restore_gain_calibrations_() {
} }
void ATM90E32Component::restore_offset_calibrations_() { void ATM90E32Component::restore_offset_calibrations_() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
for (uint8_t i = 0; i < 3; ++i) for (uint8_t i = 0; i < 3; ++i)
this->config_offset_phase_[i] = this->offset_phase_[i]; this->config_offset_phase_[i] = this->offset_phase_[i];
@@ -925,8 +912,7 @@ void ATM90E32Component::restore_offset_calibrations_() {
} }
void ATM90E32Component::restore_power_offset_calibrations_() { void ATM90E32Component::restore_power_offset_calibrations_() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
for (uint8_t i = 0; i < 3; ++i) for (uint8_t i = 0; i < 3; ++i)
this->config_power_offset_phase_[i] = this->power_offset_phase_[i]; this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
@@ -968,8 +954,7 @@ void ATM90E32Component::restore_power_offset_calibrations_() {
} }
void ATM90E32Component::clear_gain_calibrations() { void ATM90E32Component::clear_gain_calibrations() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
if (!this->using_saved_calibrations_) { if (!this->using_saved_calibrations_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
@@ -1018,8 +1003,7 @@ void ATM90E32Component::clear_gain_calibrations() {
} }
void ATM90E32Component::clear_offset_calibrations() { void ATM90E32Component::clear_offset_calibrations() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
if (!this->restored_offset_calibration_) { if (!this->restored_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
@@ -1061,8 +1045,7 @@ void ATM90E32Component::clear_offset_calibrations() {
} }
void ATM90E32Component::clear_power_offset_calibrations() { void ATM90E32Component::clear_power_offset_calibrations() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
if (!this->restored_power_offset_calibration_) { if (!this->restored_power_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
@@ -1137,8 +1120,7 @@ int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive)
} }
bool ATM90E32Component::verify_gain_writes_() { bool ATM90E32Component::verify_gain_writes_() {
char cs[GPIO_SUMMARY_MAX_LEN]; const char *cs = this->cs_summary_.c_str();
this->get_cs_summary_(cs);
bool success = true; bool success = true;
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]); uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);

View File

@@ -1,13 +1,11 @@
#pragma once #pragma once
#include <span>
#include <unordered_map> #include <unordered_map>
#include "atm90e32_reg.h" #include "atm90e32_reg.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
@@ -184,7 +182,6 @@ class ATM90E32Component : public PollingComponent,
bool verify_gain_writes_(); bool verify_gain_writes_();
bool validate_spi_read_(uint16_t expected, const char *context = nullptr); bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
void log_calibration_status_(); void log_calibration_status_();
void get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer);
struct ATM90E32Phase { struct ATM90E32Phase {
uint16_t voltage_gain_{0}; uint16_t voltage_gain_{0};
@@ -250,6 +247,7 @@ class ATM90E32Component : public PollingComponent,
ESPPreferenceObject offset_pref_; ESPPreferenceObject offset_pref_;
ESPPreferenceObject power_offset_pref_; ESPPreferenceObject power_offset_pref_;
ESPPreferenceObject gain_calibration_pref_; ESPPreferenceObject gain_calibration_pref_;
std::string cs_summary_;
sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *freq_sensor_{nullptr};
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR

View File

@@ -10,11 +10,12 @@
* - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4) * - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4)
*/ */
#include "bmp581_base.h" #include "bmp581.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
namespace esphome::bmp581_base { namespace esphome {
namespace bmp581 {
static const char *const TAG = "bmp581"; static const char *const TAG = "bmp581";
@@ -90,6 +91,7 @@ void BMP581Component::dump_config() {
break; break;
} }
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_); ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
@@ -147,7 +149,7 @@ void BMP581Component::setup() {
uint8_t chip_id; uint8_t chip_id;
// read chip id from sensor // read chip id from sensor
if (!this->bmp_read_byte(BMP581_CHIP_ID, &chip_id)) { if (!this->read_byte(BMP581_CHIP_ID, &chip_id)) {
ESP_LOGE(TAG, "Read chip ID failed"); ESP_LOGE(TAG, "Read chip ID failed");
this->error_code_ = ERROR_COMMUNICATION_FAILED; this->error_code_ = ERROR_COMMUNICATION_FAILED;
@@ -170,7 +172,7 @@ void BMP581Component::setup() {
// 3) Verify sensor status (check if NVM is okay) // // 3) Verify sensor status (check if NVM is okay) //
//////////////////////////////////////////////////// ////////////////////////////////////////////////////
if (!this->bmp_read_byte(BMP581_STATUS, &this->status_.reg)) { if (!this->read_byte(BMP581_STATUS, &this->status_.reg)) {
ESP_LOGE(TAG, "Failed to read status register"); ESP_LOGE(TAG, "Failed to read status register");
this->error_code_ = ERROR_COMMUNICATION_FAILED; this->error_code_ = ERROR_COMMUNICATION_FAILED;
@@ -357,7 +359,7 @@ bool BMP581Component::check_data_readiness_() {
uint8_t status; uint8_t status;
if (!this->bmp_read_byte(BMP581_INT_STATUS, &status)) { if (!this->read_byte(BMP581_INT_STATUS, &status)) {
ESP_LOGE(TAG, "Failed to read interrupt status register"); ESP_LOGE(TAG, "Failed to read interrupt status register");
return false; return false;
} }
@@ -398,7 +400,7 @@ bool BMP581Component::prime_iir_filter_() {
// flush the IIR filter with forced measurements (we will only flush once) // flush the IIR filter with forced measurements (we will only flush once)
this->dsp_config_.bit.iir_flush_forced_en = true; this->dsp_config_.bit.iir_flush_forced_en = true;
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) { if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
ESP_LOGE(TAG, "Failed to write IIR source register"); ESP_LOGE(TAG, "Failed to write IIR source register");
return false; return false;
@@ -428,7 +430,7 @@ bool BMP581Component::prime_iir_filter_() {
// disable IIR filter flushings on future forced measurements // disable IIR filter flushings on future forced measurements
this->dsp_config_.bit.iir_flush_forced_en = false; this->dsp_config_.bit.iir_flush_forced_en = false;
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) { if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
ESP_LOGE(TAG, "Failed to write IIR source register"); ESP_LOGE(TAG, "Failed to write IIR source register");
return false; return false;
@@ -452,7 +454,7 @@ bool BMP581Component::read_temperature_(float &temperature) {
} }
uint8_t data[3]; uint8_t data[3];
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) { if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
ESP_LOGW(TAG, "Failed to read measurement"); ESP_LOGW(TAG, "Failed to read measurement");
this->status_set_warning(); this->status_set_warning();
@@ -481,7 +483,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &
} }
uint8_t data[6]; uint8_t data[6];
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) { if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
ESP_LOGW(TAG, "Failed to read measurement"); ESP_LOGW(TAG, "Failed to read measurement");
this->status_set_warning(); this->status_set_warning();
@@ -505,7 +507,7 @@ bool BMP581Component::reset_() {
// - returns the Power-On-Reboot interrupt status, which is asserted if successful // - returns the Power-On-Reboot interrupt status, which is asserted if successful
// writes reset command to BMP's command register // writes reset command to BMP's command register
if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) { if (!this->write_byte(BMP581_COMMAND, RESET_COMMAND)) {
ESP_LOGE(TAG, "Failed to write reset command"); ESP_LOGE(TAG, "Failed to write reset command");
return false; return false;
@@ -516,7 +518,7 @@ bool BMP581Component::reset_() {
delay(3); delay(3);
// read interrupt status register // read interrupt status register
if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) { if (!this->read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
ESP_LOGE(TAG, "Failed to read interrupt status register"); ESP_LOGE(TAG, "Failed to read interrupt status register");
return false; return false;
@@ -560,7 +562,7 @@ bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter p
// BMP581_DSP register and BMP581_DSP_IIR registers are successive // BMP581_DSP register and BMP581_DSP_IIR registers are successive
// - allows us to write the IIR configuration with one command to both registers // - allows us to write the IIR configuration with one command to both registers
uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg}; uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg};
return this->bmp_write_bytes(BMP581_DSP, register_data, sizeof(register_data)); return this->write_bytes(BMP581_DSP, register_data, sizeof(register_data));
} }
bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) { bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
@@ -570,7 +572,7 @@ bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
this->int_source_.bit.drdy_data_reg_en = data_ready_enable; this->int_source_.bit.drdy_data_reg_en = data_ready_enable;
// write interrupt source register // write interrupt source register
return this->bmp_write_byte(BMP581_INT_SOURCE, this->int_source_.reg); return this->write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
} }
bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling, bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling,
@@ -581,7 +583,7 @@ bool BMP581Component::write_oversampling_settings_(Oversampling temperature_over
this->osr_config_.bit.osr_t = temperature_oversampling; this->osr_config_.bit.osr_t = temperature_oversampling;
this->osr_config_.bit.osr_p = pressure_oversampling; this->osr_config_.bit.osr_p = pressure_oversampling;
return this->bmp_write_byte(BMP581_OSR, this->osr_config_.reg); return this->write_byte(BMP581_OSR, this->osr_config_.reg);
} }
bool BMP581Component::write_power_mode_(OperationMode mode) { bool BMP581Component::write_power_mode_(OperationMode mode) {
@@ -591,7 +593,8 @@ bool BMP581Component::write_power_mode_(OperationMode mode) {
this->odr_config_.bit.pwr_mode = mode; this->odr_config_.bit.pwr_mode = mode;
// write odr register // write odr register
return this->bmp_write_byte(BMP581_ODR, this->odr_config_.reg); return this->write_byte(BMP581_ODR, this->odr_config_.reg);
} }
} // namespace esphome::bmp581_base } // namespace bmp581
} // namespace esphome

View File

@@ -3,9 +3,11 @@
#pragma once #pragma once
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
namespace esphome::bmp581_base { namespace esphome {
namespace bmp581 {
static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet) static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet)
static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command
@@ -57,7 +59,7 @@ enum IIRFilter {
IIR_FILTER_128 = 0x7 IIR_FILTER_128 = 0x7
}; };
class BMP581Component : public PollingComponent { class BMP581Component : public PollingComponent, public i2c::I2CDevice {
public: public:
void dump_config() override; void dump_config() override;
@@ -82,11 +84,6 @@ class BMP581Component : public PollingComponent {
void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; } void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; }
protected: protected:
virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0;
virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0;
virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr};
@@ -219,4 +216,5 @@ class BMP581Component : public PollingComponent {
} odr_config_ = {.reg = 0}; } odr_config_ = {.reg = 0};
}; };
} // namespace esphome::bmp581_base } // namespace bmp581
} // namespace esphome

View File

@@ -1,5 +1,164 @@
import esphome.config_validation as cv import math
CONFIG_SCHEMA = cv.invalid( import esphome.codegen as cg
"The bmp581 sensor component has been renamed to bmp581_i2c." from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
) )
CODEOWNERS = ["@kahrendt"]
DEPENDENCIES = ["i2c"]
bmp581_ns = cg.esphome_ns.namespace("bmp581")
Oversampling = bmp581_ns.enum("Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": Oversampling.OVERSAMPLING_NONE,
"2X": Oversampling.OVERSAMPLING_X2,
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32X": Oversampling.OVERSAMPLING_X32,
"64X": Oversampling.OVERSAMPLING_X64,
"128X": Oversampling.OVERSAMPLING_X128,
}
IIRFilter = bmp581_ns.enum("IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": IIRFilter.IIR_FILTER_OFF,
"2X": IIRFilter.IIR_FILTER_2,
"4X": IIRFilter.IIR_FILTER_4,
"8X": IIRFilter.IIR_FILTER_8,
"16X": IIRFilter.IIR_FILTER_16,
"32X": IIRFilter.IIR_FILTER_32,
"64X": IIRFilter.IIR_FILTER_64,
"128X": IIRFilter.IIR_FILTER_128,
}
BMP581Component = bmp581_ns.class_(
"BMP581Component", cg.PollingComponent, i2c.I2CDevice
)
def compute_measurement_conversion_time(config):
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
# Page 12 of datasheet
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.7,
"4X": 2.9,
"8X": 5.4,
"16X": 10.4,
"32X": 20.4,
"64X": 40.4,
"128X": 80.4,
}
# Page 12 of datasheet
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.1,
"4X": 1.5,
"8X": 2.1,
"16X": 3.3,
"32X": 5.8,
"64X": 10.8,
"128X": 20.8,
}
pressure_conversion_time = (
0.0 # No conversion time necessary without a pressure sensor
)
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
pressure_config.get(CONF_OVERSAMPLING)
]
temperature_conversion_time = (
1.0 # BMP581 always samples the temperature even if only reading pressure
)
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
temperature_config.get(CONF_OVERSAMPLING)
]
# Datasheet indicates a 5% possible error in each conversion time listed
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP581Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_PASCAL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x46))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
cg.add(
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))

View File

@@ -1,157 +0,0 @@
import math
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
)
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
bmp581_ns = cg.esphome_ns.namespace("bmp581_base")
Oversampling = bmp581_ns.enum("Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": Oversampling.OVERSAMPLING_NONE,
"2X": Oversampling.OVERSAMPLING_X2,
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32X": Oversampling.OVERSAMPLING_X32,
"64X": Oversampling.OVERSAMPLING_X64,
"128X": Oversampling.OVERSAMPLING_X128,
}
IIRFilter = bmp581_ns.enum("IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": IIRFilter.IIR_FILTER_OFF,
"2X": IIRFilter.IIR_FILTER_2,
"4X": IIRFilter.IIR_FILTER_4,
"8X": IIRFilter.IIR_FILTER_8,
"16X": IIRFilter.IIR_FILTER_16,
"32X": IIRFilter.IIR_FILTER_32,
"64X": IIRFilter.IIR_FILTER_64,
"128X": IIRFilter.IIR_FILTER_128,
}
BMP581Component = bmp581_ns.class_("BMP581Component", cg.PollingComponent)
def compute_measurement_conversion_time(config):
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
# Page 12 of datasheet
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.7,
"4X": 2.9,
"8X": 5.4,
"16X": 10.4,
"32X": 20.4,
"64X": 40.4,
"128X": 80.4,
}
# Page 12 of datasheet
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.1,
"4X": 1.5,
"8X": 2.1,
"16X": 3.3,
"32X": 5.8,
"64X": 10.8,
"128X": 20.8,
}
pressure_conversion_time = (
0.0 # No conversion time necessary without a pressure sensor
)
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
pressure_config.get(CONF_OVERSAMPLING)
]
temperature_conversion_time = (
1.0 # BMP581 always samples the temperature even if only reading pressure
)
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
temperature_config.get(CONF_OVERSAMPLING)
]
# Datasheet indicates a 5% possible error in each conversion time listed
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP581Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_PASCAL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
}
).extend(cv.polling_component_schema("60s"))
async def to_code_base(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
cg.add(
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))
return var

View File

@@ -1,12 +0,0 @@
#include "bmp581_i2c.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::bmp581_i2c {
void BMP581I2CComponent::dump_config() {
LOG_I2C_DEVICE(this);
BMP581Component::dump_config();
}
} // namespace esphome::bmp581_i2c

View File

@@ -1,24 +0,0 @@
#pragma once
#include "esphome/components/bmp581_base/bmp581_base.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::bmp581_i2c {
static const char *const TAG = "bmp581_i2c.sensor";
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
class BMP581I2CComponent : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
public:
bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); }
bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); }
bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
return read_bytes(a_register, data, len);
}
bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
return write_bytes(a_register, data, len);
}
void dump_config() override;
};
} // namespace esphome::bmp581_i2c

View File

@@ -1,23 +0,0 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base
AUTO_LOAD = ["bmp581_base"]
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
DEPENDENCIES = ["i2c"]
bmp581_ns = cg.esphome_ns.namespace("bmp581_i2c")
BMP581I2CComponent = bmp581_ns.class_(
"BMP581I2CComponent", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
i2c.i2c_device_schema(default_address=0x46)
).extend({cv.GenerateID(): cv.declare_id(BMP581I2CComponent)})
async def to_code(config):
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)

View File

@@ -1,8 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble_tracker from esphome.components import esp32_ble_tracker
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_BINDKEY, CONF_ID, CONF_MAC_ADDRESS from esphome.const import CONF_ID, CONF_MAC_ADDRESS
from esphome.core import HexInt
CODEOWNERS = ["@nagyrobi"] CODEOWNERS = ["@nagyrobi"]
DEPENDENCIES = ["esp32_ble_tracker"] DEPENDENCIES = ["esp32_ble_tracker"]
@@ -23,7 +22,6 @@ def bthome_mithermometer_base_schema(extra_schema=None):
{ {
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer), cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_BINDKEY): cv.bind_key,
} }
) )
.extend(BLE_DEVICE_SCHEMA) .extend(BLE_DEVICE_SCHEMA)
@@ -36,9 +34,3 @@ async def setup_bthome_mithermometer(var, config):
await cg.register_component(var, config) await cg.register_component(var, config)
await esp32_ble_tracker.register_ble_device(var, config) await esp32_ble_tracker.register_ble_device(var, config)
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
if bindkey := config.get(CONF_BINDKEY):
bindkey_bytes = [
HexInt(int(bindkey[index : index + 2], 16))
for index in range(0, len(bindkey), 2)
]
cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes)))

View File

@@ -3,23 +3,15 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <algorithm>
#include <array> #include <array>
#include <cstring>
#include <span> #include <span>
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "mbedtls/ccm.h"
namespace esphome { namespace esphome {
namespace bthome_mithermometer { namespace bthome_mithermometer {
static const char *const TAG = "bthome_mithermometer"; static const char *const TAG = "bthome_mithermometer";
static constexpr size_t BTHOME_BINDKEY_SIZE = 16;
static constexpr size_t BTHOME_NONCE_SIZE = 13;
static constexpr size_t BTHOME_MIC_SIZE = 4;
static constexpr size_t BTHOME_COUNTER_SIZE = 4;
static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) { static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{}; std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
@@ -138,10 +130,6 @@ void BTHomeMiThermometer::dump_config() {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGCONFIG(TAG, "BTHome MiThermometer"); ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_)); ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
if (this->has_bindkey_) {
char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)];
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.'));
}
LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Humidity", this->humidity_);
LOG_SENSOR(" ", "Battery Level", this->battery_level_); LOG_SENSOR(" ", "Battery Level", this->battery_level_);
@@ -162,60 +150,6 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev
return matched; return matched;
} }
void BTHomeMiThermometer::set_bindkey(std::initializer_list<uint8_t> bindkey) {
if (bindkey.size() != sizeof(this->bindkey_)) {
ESP_LOGW(TAG, "BTHome bindkey size mismatch: %zu", bindkey.size());
return;
}
std::copy(bindkey.begin(), bindkey.end(), this->bindkey_);
this->has_bindkey_ = true;
}
bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
std::vector<uint8_t> &payload) const {
if (data.size() <= 1 + BTHOME_COUNTER_SIZE + BTHOME_MIC_SIZE) {
ESP_LOGVV(TAG, "Encrypted BTHome payload too short: %zu", data.size());
return false;
}
const size_t ciphertext_size = data.size() - 1 - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE;
payload.resize(ciphertext_size);
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
mac[i] = (source_address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
}
std::array<uint8_t, BTHOME_NONCE_SIZE> nonce{};
memcpy(nonce.data(), mac.data(), mac.size());
nonce[6] = 0xD2;
nonce[7] = 0xFC;
nonce[8] = data[0];
memcpy(nonce.data() + 9, &data[data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE], BTHOME_COUNTER_SIZE);
const uint8_t *ciphertext = data.data() + 1;
const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE;
mbedtls_ccm_context ctx;
mbedtls_ccm_init(&ctx);
int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, this->bindkey_, BTHOME_BINDKEY_SIZE * 8);
if (ret) {
ESP_LOGVV(TAG, "mbedtls_ccm_setkey() failed.");
mbedtls_ccm_free(&ctx);
return false;
}
ret = mbedtls_ccm_auth_decrypt(&ctx, ciphertext_size, nonce.data(), nonce.size(), nullptr, 0, ciphertext,
payload.data(), mic, BTHOME_MIC_SIZE);
mbedtls_ccm_free(&ctx);
if (ret) {
ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret);
return false;
}
return true;
}
bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
const esp32_ble_tracker::ESPBTDevice &device) { const esp32_ble_tracker::ESPBTDevice &device) {
if (!service_data.uuid.contains(0xD2, 0xFC)) { if (!service_data.uuid.contains(0xD2, 0xFC)) {
@@ -239,88 +173,51 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
return false; return false;
} }
uint64_t source_address = device.address_uint64(); char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
bool address_matches = source_address == this->address_;
if (!is_encrypted && mac_included && data.size() >= 7) {
uint64_t advertised_address = 0;
for (int i = 5; i >= 0; i--) {
advertised_address = (advertised_address << 8) | data[1 + i];
}
address_matches = address_matches || advertised_address == this->address_;
}
if (is_encrypted && !this->has_bindkey_) {
if (address_matches) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGE(TAG, "Encrypted BTHome frame received but no bindkey configured for %s",
device.address_str_to(addr_buf));
}
return false;
}
if (!is_encrypted && this->has_bindkey_) {
if (address_matches) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGE(TAG, "Unencrypted BTHome frame received with bindkey configured for %s",
device.address_str_to(addr_buf));
}
return false;
}
std::vector<uint8_t> decrypted_payload;
const uint8_t *payload = nullptr;
size_t payload_size = 0;
if (is_encrypted) { if (is_encrypted) {
if (!this->decrypt_bthome_payload_(data, source_address, decrypted_payload)) { ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf));
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; return false;
ESP_LOGVV(TAG, "Failed to decrypt BTHome frame from %s", device.address_str_to(addr_buf));
return false;
}
payload = decrypted_payload.data();
payload_size = decrypted_payload.size();
} else {
payload = data.data() + 1;
payload_size = data.size() - 1;
} }
size_t payload_index = 1;
uint64_t source_address = device.address_uint64();
if (mac_included) { if (mac_included) {
if (payload_size < 6) { if (data.size() < 7) {
ESP_LOGVV(TAG, "BTHome payload missing MAC address"); ESP_LOGVV(TAG, "BTHome payload missing MAC address");
return false; return false;
} }
source_address = 0; source_address = 0;
for (int i = 5; i >= 0; i--) { for (int i = 5; i >= 0; i--) {
source_address = (source_address << 8) | payload[i]; source_address = (source_address << 8) | data[1 + i];
} }
payload += 6; payload_index = 7;
payload_size -= 6;
} }
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
if (source_address != this->address_) { if (source_address != this->address_) {
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address)); ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
return false; return false;
} }
if (payload_size == 0) { if (payload_index >= data.size()) {
ESP_LOGVV(TAG, "BTHome payload empty after header"); ESP_LOGVV(TAG, "BTHome payload empty after header");
return false; return false;
} }
bool reported = false; bool reported = false;
size_t offset = 0; size_t offset = payload_index;
uint8_t last_type = 0; uint8_t last_type = 0;
while (offset < payload_size) { while (offset < data.size()) {
const uint8_t obj_type = payload[offset++]; const uint8_t obj_type = data[offset++];
size_t value_length = 0; size_t value_length = 0;
bool has_length_byte = obj_type == 0x53; // text objects include explicit length bool has_length_byte = obj_type == 0x53; // text objects include explicit length
if (has_length_byte) { if (has_length_byte) {
if (offset >= payload_size) { if (offset >= data.size()) {
break; break;
} }
value_length = payload[offset++]; value_length = data[offset++];
} else { } else {
if (!get_bthome_value_length(obj_type, value_length)) { if (!get_bthome_value_length(obj_type, value_length)) {
ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type); ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
@@ -332,12 +229,12 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
break; break;
} }
if (offset + value_length > payload_size) { if (offset + value_length > data.size()) {
ESP_LOGVV(TAG, "BTHome object length exceeds payload"); ESP_LOGVV(TAG, "BTHome object length exceeds payload");
break; break;
} }
const uint8_t *value = &payload[offset]; const uint8_t *value = &data[offset];
offset += value_length; offset += value_length;
if (obj_type < last_type) { if (obj_type < last_type) {

View File

@@ -5,8 +5,6 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include <cstdint> #include <cstdint>
#include <initializer_list>
#include <vector>
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -16,7 +14,6 @@ namespace bthome_mithermometer {
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component { class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
public: public:
void set_address(uint64_t address) { this->address_ = address; } void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(std::initializer_list<uint8_t> bindkey);
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
@@ -30,13 +27,9 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi
protected: protected:
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
const esp32_ble_tracker::ESPBTDevice &device); const esp32_ble_tracker::ESPBTDevice &device);
bool decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
std::vector<uint8_t> &payload) const;
uint64_t address_{0}; uint64_t address_{0};
optional<uint8_t> last_packet_id_{}; optional<uint8_t> last_packet_id_{};
bool has_bindkey_{false};
uint8_t bindkey_[16];
sensor::Sensor *temperature_{nullptr}; sensor::Sensor *temperature_{nullptr};
sensor::Sensor *humidity_{nullptr}; sensor::Sensor *humidity_{nullptr};

View File

@@ -89,8 +89,10 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
delayMicroseconds(500); delayMicroseconds(500);
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000); delayMicroseconds(2000);
} else { } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
delayMicroseconds(1000); delayMicroseconds(1000);
} else {
delayMicroseconds(800);
} }
#ifdef USE_ESP32 #ifdef USE_ESP32

View File

@@ -1,5 +1,4 @@
#include "fingerprint_grow.h" #include "fingerprint_grow.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cinttypes> #include <cinttypes>
@@ -533,21 +532,14 @@ void FingerprintGrowComponent::sensor_sleep_() {
} }
void FingerprintGrowComponent::dump_config() { void FingerprintGrowComponent::dump_config() {
char sensing_pin_buf[GPIO_SUMMARY_MAX_LEN];
char power_pin_buf[GPIO_SUMMARY_MAX_LEN];
if (this->has_sensing_pin_) {
this->sensing_pin_->dump_summary(sensing_pin_buf, sizeof(sensing_pin_buf));
}
if (this->has_power_pin_) {
this->sensor_power_pin_->dump_summary(power_pin_buf, sizeof(power_pin_buf));
}
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"GROW_FINGERPRINT_READER:\n" "GROW_FINGERPRINT_READER:\n"
" System Identifier Code: 0x%.4X\n" " System Identifier Code: 0x%.4X\n"
" Touch Sensing Pin: %s\n" " Touch Sensing Pin: %s\n"
" Sensor Power Pin: %s", " Sensor Power Pin: %s",
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None", this->system_identifier_code_,
this->has_power_pin_ ? power_pin_buf : "None"); this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None",
this->has_power_pin_ ? this->sensor_power_pin_->dump_summary().c_str() : "None");
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) { if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_); ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
} else { } else {

View File

@@ -3,44 +3,13 @@
#if defined(USE_ARDUINO) || defined(USE_ESP32) #if defined(USE_ARDUINO) || defined(USE_ESP32)
#include <map> #include <map>
#include <IRSender.h> #include "ir_sender_esphome.h"
#include <HeatpumpIRFactory.h> #include "HeatpumpIRFactory.h"
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace heatpumpir { namespace heatpumpir {
// IRSenderESPHome - bridge between ESPHome's remote_transmitter and HeatpumpIR library
// Defined here (not in a header) to isolate HeatpumpIR's headers from the rest of ESPHome,
// as they define conflicting symbols like millis() in the global namespace.
class IRSenderESPHome : public IRSender {
public:
IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()) {}
void setFrequency(int frequency) override { // NOLINT(readability-identifier-naming)
auto *data = this->transmit_.get_data();
data->set_carrier_frequency(1000 * frequency);
}
void space(int space_length) override {
if (space_length) {
auto *data = this->transmit_.get_data();
data->space(space_length);
} else {
this->transmit_.perform();
}
}
void mark(int mark_length) override {
auto *data = this->transmit_.get_data();
data->mark(mark_length);
}
protected:
remote_base::RemoteTransmitterBase::TransmitCall transmit_;
};
static const char *const TAG = "heatpumpir.climate"; static const char *const TAG = "heatpumpir.climate";
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = { const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {

View File

@@ -0,0 +1,32 @@
#include "ir_sender_esphome.h"
#if defined(USE_ARDUINO) || defined(USE_ESP32)
namespace esphome {
namespace heatpumpir {
void IRSenderESPHome::setFrequency(int frequency) { // NOLINT(readability-identifier-naming)
auto *data = transmit_.get_data();
data->set_carrier_frequency(1000 * frequency);
}
// Send an IR 'mark' symbol, i.e. transmitter ON
void IRSenderESPHome::mark(int mark_length) {
auto *data = transmit_.get_data();
data->mark(mark_length);
}
// Send an IR 'space' symbol, i.e. transmitter OFF
void IRSenderESPHome::space(int space_length) {
if (space_length) {
auto *data = transmit_.get_data();
data->space(space_length);
} else {
transmit_.perform();
}
}
} // namespace heatpumpir
} // namespace esphome
#endif

View File

@@ -0,0 +1,25 @@
#pragma once
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include "esphome/components/remote_base/remote_base.h"
#include <IRSender.h> // arduino-heatpump library
namespace esphome {
namespace heatpumpir {
class IRSenderESPHome : public IRSender {
public:
IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()){};
void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming)
void space(int space_length) override;
void mark(int mark_length) override;
protected:
remote_base::RemoteTransmitterBase::TransmitCall transmit_;
};
} // namespace heatpumpir
} // namespace esphome
#endif

View File

@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
} }
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP; jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs); ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100); esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20);
if (err == ESP_ERR_INVALID_STATE) { if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGV(TAG, "TX to %02X failed: not acked", address); ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED; return ERROR_NOT_ACKNOWLEDGED;

View File

@@ -5,6 +5,8 @@
// Once the API is considered stable, this warning will be removed. // Once the API is considered stable, this warning will be removed.
#include "esphome/components/infrared/infrared.h" #include "esphome/components/infrared/infrared.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
#include "esphome/components/remote_receiver/remote_receiver.h"
namespace esphome::ir_rf_proxy { namespace esphome::ir_rf_proxy {

View File

@@ -391,10 +391,7 @@ void LightCall::transform_parameters_() {
min_mireds > 0.0f && max_mireds > 0.0f) { min_mireds > 0.0f && max_mireds > 0.0f) {
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
this->parent_->get_name().c_str()); this->parent_->get_name().c_str());
// Only compute cold_white/warm_white from color_temperature if they're not already explicitly set. if (this->has_color_temperature()) {
// This is important for state restoration, where both color_temperature and cold_white/warm_white
// are restored from flash - we want to preserve the saved cold_white/warm_white values.
if (this->has_color_temperature() && !this->has_cold_white() && !this->has_warm_white()) {
const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds); const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds);
const float range = max_mireds - min_mireds; const float range = max_mireds - min_mireds;
const float ww_fraction = (color_temp - min_mireds) / range; const float ww_fraction = (color_temp - min_mireds) / range;

View File

@@ -32,7 +32,7 @@ class LabelType(WidgetType):
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
"""For a text object, create and set text""" """For a text object, create and set text"""
if (value := config.get(CONF_TEXT)) is not None: if value := config.get(CONF_TEXT):
await w.set_property(CONF_TEXT, await lv_text.process(value)) await w.set_property(CONF_TEXT, await lv_text.process(value))
await w.set_property(CONF_LONG_MODE, config) await w.set_property(CONF_LONG_MODE, config)
await w.set_property(CONF_RECOLOR, config) await w.set_property(CONF_RECOLOR, config)

View File

@@ -55,44 +55,3 @@ DriverChip(
(0x35,), (0xFE,), (0x35,), (0xFE,),
], ],
) )
DriverChip(
"M5STACK-TAB5-V2",
height=1280,
width=720,
hsync_back_porch=40,
hsync_pulse_width=2,
hsync_front_porch=40,
vsync_back_porch=8,
vsync_pulse_width=2,
vsync_front_porch=220,
pclk_frequency="80MHz",
lane_bit_rate="960Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0x60, 0x71, 0x23, 0xa2),
(0x60, 0x71, 0x23, 0xa3),
(0x60, 0x71, 0x23, 0xa4),
(0xA4, 0x31),
(0xD7, 0x10, 0x0A, 0x10, 0x2A, 0x80, 0x80),
(0x90, 0x71, 0x23, 0x5A, 0x20, 0x24, 0x09, 0x09),
(0xA3, 0x80, 0x01, 0x88, 0x30, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x4F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x6F, 0x58, 0x00, 0x00, 0x00, 0xFF),
(0xA6, 0x03, 0x00, 0x24, 0x55, 0x36, 0x00, 0x39, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x55, 0x38, 0x00, 0x37, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x11, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0xEC, 0x11, 0x00, 0x03, 0x00, 0x03, 0x6E, 0x6E, 0xFF, 0xFF, 0x00, 0x08, 0x80, 0x08, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00),
(0xA7, 0x19, 0x19, 0x80, 0x64, 0x40, 0x07, 0x16, 0x40, 0x00, 0x44, 0x03, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x25, 0x34, 0x40, 0x00, 0x02, 0x01, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x6E, 0x6E, 0x84, 0xFF, 0x08, 0x80, 0x44),
(0xAC, 0x03, 0x19, 0x19, 0x18, 0x18, 0x06, 0x13, 0x13, 0x11, 0x11, 0x08, 0x08, 0x0A, 0x0A, 0x1C, 0x1C, 0x07, 0x07, 0x00, 0x00, 0x02, 0x02, 0x01, 0x19, 0x19, 0x18, 0x18, 0x06, 0x12, 0x12, 0x10, 0x10, 0x09, 0x09, 0x0B, 0x0B, 0x1C, 0x1C, 0x07, 0x07, 0x03, 0x03, 0x01, 0x01),
(0xAD, 0xF0, 0x00, 0x46, 0x00, 0x03, 0x50, 0x50, 0xFF, 0xFF, 0xF0, 0x40, 0x06, 0x01, 0x07, 0x42, 0x42, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF),
(0xAE, 0xFE, 0x3F, 0x3F, 0xFE, 0x3F, 0x3F, 0x00),
(0xB2, 0x15, 0x19, 0x05, 0x23, 0x49, 0xAF, 0x03, 0x2E, 0x5C, 0xD2, 0xFF, 0x10, 0x20, 0xFD, 0x20, 0xC0, 0x00),
(0xE8, 0x20, 0x6F, 0x04, 0x97, 0x97, 0x3E, 0x04, 0xDC, 0xDC, 0x3E, 0x06, 0xFA, 0x26, 0x3E),
(0x75, 0x03, 0x04),
(0xE7, 0x3B, 0x00, 0x00, 0x7C, 0xA1, 0x8C, 0x20, 0x1A, 0xF0, 0xB1, 0x50, 0x00, 0x50, 0xB1, 0x50, 0xB1, 0x50, 0xD8, 0x00, 0x55, 0x00, 0xB1, 0x00, 0x45, 0xC9, 0x6A, 0xFF, 0x5A, 0xD8, 0x18, 0x88, 0x15, 0xB1, 0x01, 0x01, 0x77),
(0xEA, 0x13, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x2C),
(0xB0, 0x22, 0x43, 0x11, 0x61, 0x25, 0x43, 0x43),
(0xb7, 0x00, 0x00, 0x73, 0x73),
(0xBF, 0xA6, 0xAA),
(0xA9, 0x00, 0x00, 0x73, 0xFF, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03),
(0xC8, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF),
(0xC9, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF),
],
)

View File

@@ -1,11 +1,9 @@
#ifdef USE_ESP32_VARIANT_ESP32S3 #ifdef USE_ESP32_VARIANT_ESP32S3
#include "mipi_rgb.h" #include "mipi_rgb.h"
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esp_lcd_panel_rgb.h" #include "esp_lcd_panel_rgb.h"
#include <span>
namespace esphome { namespace esphome {
namespace mipi_rgb { namespace mipi_rgb {
@@ -345,27 +343,19 @@ int MipiRgb::get_height() {
} }
} }
static const char *get_pin_name(GPIOPin *pin, std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) { static std::string get_pin_name(GPIOPin *pin) {
if (pin == nullptr) if (pin == nullptr)
return "None"; return "None";
pin->dump_summary(buffer.data(), buffer.size()); return pin->dump_summary();
return buffer.data();
} }
void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) { void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) {
char pin_summary[GPIO_SUMMARY_MAX_LEN];
for (uint8_t i = start; i != end; i++) { for (uint8_t i = start; i != end; i++) {
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary)); ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str());
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, pin_summary);
} }
} }
void MipiRgb::dump_config() { void MipiRgb::dump_config() {
char reset_buf[GPIO_SUMMARY_MAX_LEN];
char de_buf[GPIO_SUMMARY_MAX_LEN];
char pclk_buf[GPIO_SUMMARY_MAX_LEN];
char hsync_buf[GPIO_SUMMARY_MAX_LEN];
char vsync_buf[GPIO_SUMMARY_MAX_LEN];
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"MIPI_RGB LCD" "MIPI_RGB LCD"
"\n Model: %s" "\n Model: %s"
@@ -389,9 +379,9 @@ void MipiRgb::dump_config() {
this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_), this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_),
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_), this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_),
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_, reset_buf), (unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(),
get_pin_name(this->de_pin_, de_buf), get_pin_name(this->pclk_pin_, pclk_buf), get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
get_pin_name(this->hsync_pin_, hsync_buf), get_pin_name(this->vsync_pin_, vsync_buf)); get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str());
this->dump_pins_(8, 13, "Blue", 0); this->dump_pins_(8, 13, "Blue", 0);
this->dump_pins_(13, 16, "Green", 0); this->dump_pins_(13, 16, "Green", 0);

View File

@@ -55,7 +55,6 @@ st7701s = ST7701S(
pclk_frequency="16MHz", pclk_frequency="16MHz",
pclk_inverted=True, pclk_inverted=True,
initsequence=( initsequence=(
(0x01,), # Software Reset
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0 (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0
(0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05), (0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05),
(0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,), (0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,),

View File

@@ -279,7 +279,7 @@ def modbus_calc_properties(config):
if isinstance(value, str): if isinstance(value, str):
value = value.encode() value = value.encode()
config[CONF_ADDRESS] = binascii.crc_hqx(value, 0) config[CONF_ADDRESS] = binascii.crc_hqx(value, 0)
config[CONF_REGISTER_TYPE] = cv.enum(MODBUS_REGISTER_TYPE)("custom") config[CONF_REGISTER_TYPE] = ModbusRegisterType.CUSTOM
config[CONF_FORCE_NEW_RANGE] = True config[CONF_FORCE_NEW_RANGE] = True
return byte_offset, reg_count return byte_offset, reg_count

View File

@@ -133,17 +133,14 @@ void RD03DComponent::process_frame_() {
uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE); uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE);
// Extract raw bytes for this target // Extract raw bytes for this target
// Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution,
// actual radar output has Resolution before Speed (verified empirically -
// stationary targets were showing non-zero speed with original field order)
uint8_t x_low = this->buffer_[offset + 0]; uint8_t x_low = this->buffer_[offset + 0];
uint8_t x_high = this->buffer_[offset + 1]; uint8_t x_high = this->buffer_[offset + 1];
uint8_t y_low = this->buffer_[offset + 2]; uint8_t y_low = this->buffer_[offset + 2];
uint8_t y_high = this->buffer_[offset + 3]; uint8_t y_high = this->buffer_[offset + 3];
uint8_t res_low = this->buffer_[offset + 4]; uint8_t speed_low = this->buffer_[offset + 4];
uint8_t res_high = this->buffer_[offset + 5]; uint8_t speed_high = this->buffer_[offset + 5];
uint8_t speed_low = this->buffer_[offset + 6]; uint8_t res_low = this->buffer_[offset + 6];
uint8_t speed_high = this->buffer_[offset + 7]; uint8_t res_high = this->buffer_[offset + 7];
// Decode values per RD-03D format // Decode values per RD-03D format
int16_t x = decode_value(x_low, x_high); int16_t x = decode_value(x_low, x_high);

View File

@@ -1,6 +1,5 @@
#ifdef USE_ESP32_VARIANT_ESP32S3 #ifdef USE_ESP32_VARIANT_ESP32S3
#include "rpi_dpi_rgb.h" #include "rpi_dpi_rgb.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@@ -135,11 +134,8 @@ void RpiDpiRgb::dump_config() {
LOG_PIN(" Enable Pin: ", this->enable_pin_); LOG_PIN(" Enable Pin: ", this->enable_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_);
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
char pin_summary[GPIO_SUMMARY_MAX_LEN]; for (size_t i = 0; i != data_pin_count; i++)
for (size_t i = 0; i != data_pin_count; i++) { ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
}
} }
void RpiDpiRgb::reset_display_() const { void RpiDpiRgb::reset_display_() const {

View File

@@ -30,19 +30,6 @@ static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; //
static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor 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 static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
static const LogString *type_to_string(Sen5xType type) {
switch (type) {
case Sen5xType::SEN50:
return LOG_STR("SEN50");
case Sen5xType::SEN54:
return LOG_STR("SEN54");
case Sen5xType::SEN55:
return LOG_STR("SEN55");
default:
return LOG_STR("UNKNOWN");
}
}
static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) { static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) {
switch (mode) { switch (mode) {
case LOW_ACCELERATION: case LOW_ACCELERATION:
@@ -56,15 +43,6 @@ static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) {
} }
} }
// This function performs an in-place conversion of the provided buffer
// from uint16_t values to big endianness
static inline const char *sensirion_convert_to_string_in_place(uint16_t *array, size_t length) {
for (size_t i = 0; i < length; i++) {
array[i] = convert_big_endian(array[i]);
}
return reinterpret_cast<const char *>(array);
}
void SEN5XComponent::setup() { void SEN5XComponent::setup() {
// the sensor needs 1000 ms to enter the idle state // the sensor needs 1000 ms to enter the idle state
this->set_timeout(1000, [this]() { this->set_timeout(1000, [this]() {
@@ -97,18 +75,18 @@ void SEN5XComponent::setup() {
stop_measurement_delay = 200; stop_measurement_delay = 200;
} }
this->set_timeout(stop_measurement_delay, [this]() { this->set_timeout(stop_measurement_delay, [this]() {
// note: serial number register is actually 32-bytes long but we grab only the first 16-bytes, uint16_t raw_serial_number[3];
// this appears to be all that Sensirion uses for serial numbers, this could change if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) {
uint16_t raw_serial_number[8];
if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 8, 20)) {
ESP_LOGE(TAG, "Failed to read serial number"); ESP_LOGE(TAG, "Failed to read serial number");
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
this->mark_failed(); this->mark_failed();
return; return;
} }
const char *serial_number = sensirion_convert_to_string_in_place(raw_serial_number, 8); this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF);
snprintf(this->serial_number_, sizeof(this->serial_number_), "%s", serial_number); this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF);
ESP_LOGV(TAG, "Serial number %s", this->serial_number_); this->serial_number_[2] = static_cast<uint16_t>(raw_serial_number[1] >> 8);
ESP_LOGV(TAG, "Serial number %02d.%02d.%02d", this->serial_number_[0], this->serial_number_[1],
this->serial_number_[2]);
uint16_t raw_product_name[16]; uint16_t raw_product_name[16];
if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
@@ -117,35 +95,50 @@ void SEN5XComponent::setup() {
this->mark_failed(); this->mark_failed();
return; return;
} }
const char *product_name = sensirion_convert_to_string_in_place(raw_product_name, 16); // 2 ASCII bytes are encoded in an int
if (strncmp(product_name, "SEN50", 5) == 0) { const uint16_t *current_int = raw_product_name;
this->type_ = Sen5xType::SEN50; char current_char;
} else if (strncmp(product_name, "SEN54", 5) == 0) { uint8_t max = 16;
this->type_ = Sen5xType::SEN54; do {
} else if (strncmp(product_name, "SEN55", 5) == 0) { // first char
this->type_ = Sen5xType::SEN55; current_char = *current_int >> 8;
} else { if (current_char) {
this->type_ = Sen5xType::UNKNOWN; this->product_name_.push_back(current_char);
ESP_LOGE(TAG, "Unknown product name: %.32s", product_name); // second char
this->error_code_ = PRODUCT_NAME_FAILED; current_char = *current_int & 0xFF;
this->mark_failed(); if (current_char) {
return; this->product_name_.push_back(current_char);
} }
}
current_int++;
} while (current_char && --max);
ESP_LOGD(TAG, "Type: %s", LOG_STR_ARG(type_to_string(this->type_))); Sen5xType sen5x_type = UNKNOWN;
if (this->humidity_sensor_ && this->type_ == Sen5xType::SEN50) { if (this->product_name_ == "SEN50") {
sen5x_type = SEN50;
} else {
if (this->product_name_ == "SEN54") {
sen5x_type = SEN54;
} else {
if (this->product_name_ == "SEN55") {
sen5x_type = SEN55;
}
}
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
}
if (this->humidity_sensor_ && sen5x_type == SEN50) {
ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55"); ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55");
this->humidity_sensor_ = nullptr; // mark as not used this->humidity_sensor_ = nullptr; // mark as not used
} }
if (this->temperature_sensor_ && this->type_ == Sen5xType::SEN50) { if (this->temperature_sensor_ && sen5x_type == SEN50) {
ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55"); ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55");
this->temperature_sensor_ = nullptr; // mark as not used this->temperature_sensor_ = nullptr; // mark as not used
} }
if (this->voc_sensor_ && this->type_ == Sen5xType::SEN50) { if (this->voc_sensor_ && sen5x_type == SEN50) {
ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55"); ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55");
this->voc_sensor_ = nullptr; // mark as not used this->voc_sensor_ = nullptr; // mark as not used
} }
if (this->nox_sensor_ && this->type_ != Sen5xType::SEN55) { if (this->nox_sensor_ && sen5x_type != SEN55) {
ESP_LOGE(TAG, "NOx requires a SEN55"); ESP_LOGE(TAG, "NOx requires a SEN55");
this->nox_sensor_ = nullptr; // mark as not used this->nox_sensor_ = nullptr; // mark as not used
} }
@@ -160,25 +153,43 @@ void SEN5XComponent::setup() {
ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_); ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_);
if (this->voc_sensor_ && this->store_baseline_) { if (this->voc_sensor_ && this->store_baseline_) {
// Hash with serial number, serial numbers are unique, so multiple sensors can be used without conflict uint32_t combined_serial =
uint32_t hash = fnv1a_hash(this->serial_number_); encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]);
this->pref_ = global_preferences->make_preference<uint16_t[4]>(hash, true); // Hash with config hash, version, and serial number
this->voc_baseline_time_ = App.get_loop_component_start_time(); // This ensures the baseline storage is cleared after OTA
if (this->pref_.load(&this->voc_baseline_state_)) { // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) { uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial);
ESP_LOGE(TAG, "VOC Baseline State write to sensor failed"); this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
} else {
ESP_LOGV(TAG, "VOC Baseline State loaded"); if (this->pref_.load(&this->voc_baselines_storage_)) {
delay(20); ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
uint16_t states[4];
states[0] = this->voc_baselines_storage_.state0 >> 16;
states[1] = this->voc_baselines_storage_.state0 & 0xFFFF;
states[2] = this->voc_baselines_storage_.state1 >> 16;
states[3] = this->voc_baselines_storage_.state1 & 0xFFFF;
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
} }
} }
} }
bool result; bool result;
if (this->auto_cleaning_interval_.has_value()) { if (this->auto_cleaning_interval_.has_value()) {
// override default value // override default value
result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value()); result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
} else { } else {
result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL); result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
} }
if (result) { if (result) {
delay(20); delay(20);
@@ -265,10 +276,11 @@ void SEN5XComponent::dump_config() {
} }
} }
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Type: %s\n" " Product name: %s\n"
" Firmware version: %d\n" " Firmware version: %d\n"
" Serial number: %s", " Serial number %02d.%02d.%02d",
LOG_STR_ARG(type_to_string(this->type_)), this->firmware_version_, this->serial_number_); this->product_name_.c_str(), this->firmware_version_, this->serial_number_[0], this->serial_number_[1],
this->serial_number_[2]);
if (this->auto_cleaning_interval_.has_value()) { if (this->auto_cleaning_interval_.has_value()) {
ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value()); ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value());
} }
@@ -276,14 +288,6 @@ void SEN5XComponent::dump_config() {
ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s", ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s",
LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value()))); LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value())));
} }
if (this->voc_sensor_) {
char hex_buf[5 * 4];
format_hex_pretty_to(hex_buf, this->voc_baseline_state_, 4, 0);
ESP_LOGCONFIG(TAG,
" Store Baseline: %s\n"
" State: %s\n",
TRUEFALSE(this->store_baseline_), hex_buf);
}
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
@@ -300,6 +304,36 @@ void SEN5XComponent::update() {
return; return;
} }
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
// run it a bit later to avoid adding a delay here
this->set_timeout(550, [this]() {
uint16_t states[4];
if (this->read_data(states, 4)) {
uint32_t state0 = states[0] << 16 | states[1];
uint32_t state1 = states[2] << 16 | states[3];
if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
MAXIMUM_STORAGE_DIFF ||
(uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->voc_baselines_storage_.state0 = state0;
this->voc_baselines_storage_.state1 = state1;
if (this->pref_.save(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
});
}
}
if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) { if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
this->status_set_warning(); this->status_set_warning();
ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_); ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_);
@@ -368,29 +402,7 @@ void SEN5XComponent::update() {
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();
if (!this->voc_sensor_ || !this->store_baseline_ ||
(App.get_loop_component_start_time() - this->voc_baseline_time_) < SHORTEST_BASELINE_STORE_INTERVAL) {
this->status_clear_warning();
} else {
this->voc_baseline_time_ = App.get_loop_component_start_time();
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
this->status_set_warning();
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
} else {
this->set_timeout(20, [this]() {
if (!this->read_data(this->voc_baseline_state_, 4)) {
this->status_set_warning();
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
} else {
if (this->pref_.save(&this->voc_baseline_state_)) {
ESP_LOGD(TAG, "VOC Baseline State saved");
}
this->status_clear_warning();
}
});
}
}
}); });
} }

View File

@@ -24,7 +24,10 @@ enum RhtAccelerationMode : uint16_t {
HIGH_ACCELERATION = 2, HIGH_ACCELERATION = 2,
}; };
enum class Sen5xType : uint8_t { SEN50, SEN54, SEN55, UNKNOWN }; struct Sen5xBaselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
struct GasTuning { struct GasTuning {
uint16_t index_offset; uint16_t index_offset;
@@ -41,9 +44,11 @@ struct TemperatureCompensation {
uint16_t time_constant; uint16_t time_constant;
}; };
// Shortest time interval of 2H (in milliseconds) for storing baseline values. // Shortest time interval of 3H for storing baseline values.
// Prevents wear of the flash because of too many write operations // Prevents wear of the flash because of too many write operations
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 2 * 60 * 60 * 1000; static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
// Store anyway if the baseline difference exceeds the max storage diff value
static const uint32_t MAXIMUM_STORAGE_DIFF = 50;
class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
public: public:
@@ -51,20 +56,20 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
void dump_config() override; void dump_config() override;
void update() override; void update() override;
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { this->pm_1_0_sensor_ = pm_1_0; } enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN };
void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { this->pm_2_5_sensor_ = pm_2_5; }
void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { this->pm_4_0_sensor_ = pm_4_0; }
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { this->pm_10_0_sensor_ = pm_10_0; }
void set_voc_sensor(sensor::Sensor *voc_sensor) { this->voc_sensor_ = voc_sensor; } void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; }
void set_nox_sensor(sensor::Sensor *nox_sensor) { this->nox_sensor_ = nox_sensor; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; }
void set_store_baseline(bool store_baseline) { this->store_baseline_ = store_baseline; }
void set_acceleration_mode(RhtAccelerationMode mode) { this->acceleration_mode_ = mode; } void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
this->auto_cleaning_interval_ = auto_cleaning_interval; void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
} void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; }
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; }
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) { uint16_t std_initial, uint16_t gain_factor) {
@@ -75,7 +80,7 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
tuning_params.gating_max_duration_minutes = gating_max_duration_minutes; tuning_params.gating_max_duration_minutes = gating_max_duration_minutes;
tuning_params.std_initial = std_initial; tuning_params.std_initial = std_initial;
tuning_params.gain_factor = gain_factor; tuning_params.gain_factor = gain_factor;
this->voc_tuning_params_ = tuning_params; voc_tuning_params_ = tuning_params;
} }
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
@@ -87,14 +92,14 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
tuning_params.gating_max_duration_minutes = gating_max_duration_minutes; tuning_params.gating_max_duration_minutes = gating_max_duration_minutes;
tuning_params.std_initial = 50; tuning_params.std_initial = 50;
tuning_params.gain_factor = gain_factor; tuning_params.gain_factor = gain_factor;
this->nox_tuning_params_ = tuning_params; nox_tuning_params_ = tuning_params;
} }
void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) { void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) {
TemperatureCompensation temp_comp; TemperatureCompensation temp_comp;
temp_comp.offset = offset * 200; temp_comp.offset = offset * 200;
temp_comp.normalized_offset_slope = normalized_offset_slope * 10000; temp_comp.normalized_offset_slope = normalized_offset_slope * 10000;
temp_comp.time_constant = time_constant; temp_comp.time_constant = time_constant;
this->temperature_compensation_ = temp_comp; temperature_compensation_ = temp_comp;
} }
bool start_fan_cleaning(); bool start_fan_cleaning();
@@ -102,12 +107,10 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
bool write_temperature_compensation_(const TemperatureCompensation &compensation); bool write_temperature_compensation_(const TemperatureCompensation &compensation);
char serial_number_[17] = "UNKNOWN"; uint32_t seconds_since_last_store_;
uint16_t voc_baseline_state_[4]{0};
uint32_t voc_baseline_time_;
uint16_t firmware_version_; uint16_t firmware_version_;
Sen5xType type_{Sen5xType::UNKNOWN};
ERRORCODE error_code_; ERRORCODE error_code_;
uint8_t serial_number_[4];
bool initialized_{false}; bool initialized_{false};
bool store_baseline_; bool store_baseline_;
@@ -128,6 +131,8 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
optional<GasTuning> nox_tuning_params_; optional<GasTuning> nox_tuning_params_;
optional<TemperatureCompensation> temperature_compensation_; optional<TemperatureCompensation> temperature_compensation_;
ESPPreferenceObject pref_; ESPPreferenceObject pref_;
std::string product_name_;
Sen5xBaselines voc_baselines_storage_;
}; };
} // namespace sen5x } // namespace sen5x

View File

@@ -210,7 +210,6 @@ SENSOR_MAP = {
SETTING_MAP = { SETTING_MAP = {
CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval", CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval",
CONF_ACCELERATION_MODE: "set_acceleration_mode", CONF_ACCELERATION_MODE: "set_acceleration_mode",
CONF_STORE_BASELINE: "set_store_baseline",
} }

View File

@@ -39,23 +39,42 @@ bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) {
*/ */
bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data,
const uint8_t data_len) { const uint8_t data_len) {
uint8_t temp_stack[BUFFER_STACK_SIZE];
std::unique_ptr<uint8_t[]> temp_heap;
uint8_t *temp;
size_t required_buffer_len = data_len * 3 + 2; size_t required_buffer_len = data_len * 3 + 2;
SmallBufferWithHeapFallback<BUFFER_STACK_SIZE> buffer(required_buffer_len);
uint8_t *temp = buffer.get(); // Is a dynamic allocation required ?
if (required_buffer_len >= BUFFER_STACK_SIZE) {
temp_heap = std::unique_ptr<uint8_t[]>(new uint8_t[required_buffer_len]);
temp = temp_heap.get();
} else {
temp = temp_stack;
}
// First byte or word is the command // First byte or word is the command
uint8_t raw_idx = 0; uint8_t raw_idx = 0;
if (command_len == 1) { if (command_len == 1) {
temp[raw_idx++] = command & 0xFF; temp[raw_idx++] = command & 0xFF;
} else { } else {
// command is 2 bytes // command is 2 bytes
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
temp[raw_idx++] = command >> 8; temp[raw_idx++] = command >> 8;
temp[raw_idx++] = command & 0xFF; temp[raw_idx++] = command & 0xFF;
#else
temp[raw_idx++] = command & 0xFF;
temp[raw_idx++] = command >> 8;
#endif
} }
// add parameters followed by crc // add parameters followed by crc
// skipped if len == 0 // skipped if len == 0
for (size_t i = 0; i < data_len; i++) { for (size_t i = 0; i < data_len; i++) {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
temp[raw_idx++] = data[i] >> 8; temp[raw_idx++] = data[i] >> 8;
temp[raw_idx++] = data[i] & 0xFF; temp[raw_idx++] = data[i] & 0xFF;
#else
temp[raw_idx++] = data[i] & 0xFF;
temp[raw_idx++] = data[i] >> 8;
#endif
// Use MSB first since Sensirion devices use CRC-8 with MSB first // Use MSB first since Sensirion devices use CRC-8 with MSB first
uint8_t crc = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true); uint8_t crc = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true);
temp[raw_idx++] = crc; temp[raw_idx++] = crc;

View File

@@ -445,18 +445,22 @@ optional<float> CalibratePolynomialFilter::new_value(float value) {
ClampFilter::ClampFilter(float min, float max, bool ignore_out_of_range) ClampFilter::ClampFilter(float min, float max, bool ignore_out_of_range)
: min_(min), max_(max), ignore_out_of_range_(ignore_out_of_range) {} : min_(min), max_(max), ignore_out_of_range_(ignore_out_of_range) {}
optional<float> ClampFilter::new_value(float value) { optional<float> ClampFilter::new_value(float value) {
if (std::isfinite(this->min_) && !(value >= this->min_)) { if (std::isfinite(value)) {
if (this->ignore_out_of_range_) { if (std::isfinite(this->min_) && value < this->min_) {
return {}; if (this->ignore_out_of_range_) {
return {};
} else {
return this->min_;
}
} }
return this->min_;
}
if (std::isfinite(this->max_) && !(value <= this->max_)) { if (std::isfinite(this->max_) && value > this->max_) {
if (this->ignore_out_of_range_) { if (this->ignore_out_of_range_) {
return {}; return {};
} else {
return this->max_;
}
} }
return this->max_;
} }
return value; return value;
} }

View File

@@ -1,7 +1,6 @@
#include "slow_pwm_output.h" #include "slow_pwm_output.h"
#include "esphome/core/application.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace slow_pwm { namespace slow_pwm {
@@ -21,9 +20,7 @@ void SlowPWMOutput::set_output_state_(bool new_state) {
} }
if (new_state != current_state_) { if (new_state != current_state_) {
if (this->pin_) { if (this->pin_) {
char pin_summary[GPIO_SUMMARY_MAX_LEN]; ESP_LOGV(TAG, "Switching output pin %s to %s", this->pin_->dump_summary().c_str(), ONOFF(new_state));
this->pin_->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGV(TAG, "Switching output pin %s to %s", pin_summary, ONOFF(new_state));
} else { } else {
ESP_LOGV(TAG, "Switching to %s", ONOFF(new_state)); ESP_LOGV(TAG, "Switching to %s", ONOFF(new_state));
} }

View File

@@ -1,6 +1,5 @@
#ifdef USE_ESP32_VARIANT_ESP32S3 #ifdef USE_ESP32_VARIANT_ESP32S3
#include "st7701s.h" #include "st7701s.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@@ -184,11 +183,8 @@ void ST7701S::dump_config() {
LOG_PIN(" DE Pin: ", this->de_pin_); LOG_PIN(" DE Pin: ", this->de_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_);
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
char pin_summary[GPIO_SUMMARY_MAX_LEN]; for (size_t i = 0; i != data_pin_count; i++)
for (size_t i = 0; i != data_pin_count; i++) { ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
}
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000)); ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000));
} }

View File

@@ -1,63 +0,0 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@linkedupbits"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
CONF_SY6970_ID = "sy6970_id"
CONF_ENABLE_STATUS_LED = "enable_status_led"
CONF_INPUT_CURRENT_LIMIT = "input_current_limit"
CONF_CHARGE_VOLTAGE = "charge_voltage"
CONF_CHARGE_CURRENT = "charge_current"
CONF_PRECHARGE_CURRENT = "precharge_current"
CONF_CHARGE_ENABLED = "charge_enabled"
CONF_ENABLE_ADC = "enable_adc"
sy6970_ns = cg.esphome_ns.namespace("sy6970")
SY6970Component = sy6970_ns.class_(
"SY6970Component", cg.PollingComponent, i2c.I2CDevice
)
SY6970Listener = sy6970_ns.class_("SY6970Listener")
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SY6970Component),
cv.Optional(CONF_ENABLE_STATUS_LED, default=True): cv.boolean,
cv.Optional(CONF_INPUT_CURRENT_LIMIT, default=500): cv.int_range(
min=100, max=3200
),
cv.Optional(CONF_CHARGE_VOLTAGE, default=4208): cv.int_range(
min=3840, max=4608
),
cv.Optional(CONF_CHARGE_CURRENT, default=2048): cv.int_range(
min=0, max=5056
),
cv.Optional(CONF_PRECHARGE_CURRENT, default=128): cv.int_range(
min=64, max=1024
),
cv.Optional(CONF_CHARGE_ENABLED, default=True): cv.boolean,
cv.Optional(CONF_ENABLE_ADC, default=True): cv.boolean,
}
)
.extend(cv.polling_component_schema("5s"))
.extend(i2c.i2c_device_schema(0x6A))
)
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_ENABLE_STATUS_LED],
config[CONF_INPUT_CURRENT_LIMIT],
config[CONF_CHARGE_VOLTAGE],
config[CONF_CHARGE_CURRENT],
config[CONF_PRECHARGE_CURRENT],
config[CONF_CHARGE_ENABLED],
config[CONF_ENABLE_ADC],
)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View File

@@ -1,56 +0,0 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_POWER
from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns
DEPENDENCIES = ["sy6970"]
CONF_VBUS_CONNECTED = "vbus_connected"
CONF_CHARGING = "charging"
CONF_CHARGE_DONE = "charge_done"
SY6970VbusConnectedBinarySensor = sy6970_ns.class_(
"SY6970VbusConnectedBinarySensor", binary_sensor.BinarySensor
)
SY6970ChargingBinarySensor = sy6970_ns.class_(
"SY6970ChargingBinarySensor", binary_sensor.BinarySensor
)
SY6970ChargeDoneBinarySensor = sy6970_ns.class_(
"SY6970ChargeDoneBinarySensor", binary_sensor.BinarySensor
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component),
cv.Optional(CONF_VBUS_CONNECTED): binary_sensor.binary_sensor_schema(
SY6970VbusConnectedBinarySensor,
device_class=DEVICE_CLASS_CONNECTIVITY,
),
cv.Optional(CONF_CHARGING): binary_sensor.binary_sensor_schema(
SY6970ChargingBinarySensor,
device_class=DEVICE_CLASS_POWER,
),
cv.Optional(CONF_CHARGE_DONE): binary_sensor.binary_sensor_schema(
SY6970ChargeDoneBinarySensor,
device_class=DEVICE_CLASS_POWER,
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_SY6970_ID])
if vbus_connected_config := config.get(CONF_VBUS_CONNECTED):
sens = await binary_sensor.new_binary_sensor(vbus_connected_config)
cg.add(parent.add_listener(sens))
if charging_config := config.get(CONF_CHARGING):
sens = await binary_sensor.new_binary_sensor(charging_config)
cg.add(parent.add_listener(sens))
if charge_done_config := config.get(CONF_CHARGE_DONE):
sens = await binary_sensor.new_binary_sensor(charge_done_config)
cg.add(parent.add_listener(sens))

View File

@@ -1,43 +0,0 @@
#pragma once
#include "../sy6970.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::sy6970 {
template<uint8_t REG, uint8_t SHIFT, uint8_t MASK, uint8_t TRUE_VALUE>
class StatusBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t value = (data.registers[REG] >> SHIFT) & MASK;
this->publish_state(value == TRUE_VALUE);
}
};
template<uint8_t REG, uint8_t SHIFT, uint8_t MASK, uint8_t FALSE_VALUE>
class InverseStatusBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t value = (data.registers[REG] >> SHIFT) & MASK;
this->publish_state(value != FALSE_VALUE);
}
};
// Custom binary sensor for charging (true when pre-charge or fast charge)
class SY6970ChargingBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t chrg_stat = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03;
bool charging = chrg_stat != CHARGE_STATUS_NOT_CHARGING && chrg_stat != CHARGE_STATUS_CHARGE_DONE;
this->publish_state(charging);
}
};
// Specialized sensor types using templates
// VBUS connected: BUS_STATUS != NO_INPUT
using SY6970VbusConnectedBinarySensor = InverseStatusBinarySensor<SY6970_REG_STATUS, 5, 0x07, BUS_STATUS_NO_INPUT>;
// Charge done: CHARGE_STATUS == CHARGE_DONE
using SY6970ChargeDoneBinarySensor = StatusBinarySensor<SY6970_REG_STATUS, 3, 0x03, CHARGE_STATUS_CHARGE_DONE>;
} // namespace esphome::sy6970

View File

@@ -1,95 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_BATTERY_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
UNIT_MILLIAMP,
UNIT_VOLT,
)
from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns
DEPENDENCIES = ["sy6970"]
CONF_VBUS_VOLTAGE = "vbus_voltage"
CONF_SYSTEM_VOLTAGE = "system_voltage"
CONF_CHARGE_CURRENT = "charge_current"
CONF_PRECHARGE_CURRENT = "precharge_current"
SY6970VbusVoltageSensor = sy6970_ns.class_("SY6970VbusVoltageSensor", sensor.Sensor)
SY6970BatteryVoltageSensor = sy6970_ns.class_(
"SY6970BatteryVoltageSensor", sensor.Sensor
)
SY6970SystemVoltageSensor = sy6970_ns.class_("SY6970SystemVoltageSensor", sensor.Sensor)
SY6970ChargeCurrentSensor = sy6970_ns.class_("SY6970ChargeCurrentSensor", sensor.Sensor)
SY6970PrechargeCurrentSensor = sy6970_ns.class_(
"SY6970PrechargeCurrentSensor", sensor.Sensor
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component),
cv.Optional(CONF_VBUS_VOLTAGE): sensor.sensor_schema(
SY6970VbusVoltageSensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
SY6970BatteryVoltageSensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SYSTEM_VOLTAGE): sensor.sensor_schema(
SY6970SystemVoltageSensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CHARGE_CURRENT): sensor.sensor_schema(
SY6970ChargeCurrentSensor,
unit_of_measurement=UNIT_MILLIAMP,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRECHARGE_CURRENT): sensor.sensor_schema(
SY6970PrechargeCurrentSensor,
unit_of_measurement=UNIT_MILLIAMP,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_SY6970_ID])
if vbus_voltage_config := config.get(CONF_VBUS_VOLTAGE):
sens = await sensor.new_sensor(vbus_voltage_config)
cg.add(parent.add_listener(sens))
if battery_voltage_config := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(battery_voltage_config)
cg.add(parent.add_listener(sens))
if system_voltage_config := config.get(CONF_SYSTEM_VOLTAGE):
sens = await sensor.new_sensor(system_voltage_config)
cg.add(parent.add_listener(sens))
if charge_current_config := config.get(CONF_CHARGE_CURRENT):
sens = await sensor.new_sensor(charge_current_config)
cg.add(parent.add_listener(sens))
if precharge_current_config := config.get(CONF_PRECHARGE_CURRENT):
sens = await sensor.new_sensor(precharge_current_config)
cg.add(parent.add_listener(sens))

View File

@@ -1,46 +0,0 @@
#pragma once
#include "../sy6970.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::sy6970 {
// Template for voltage sensors (converts mV to V)
template<uint8_t REG, uint8_t MASK, uint16_t BASE, uint16_t STEP>
class VoltageSensor : public SY6970Listener, public sensor::Sensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t val = data.registers[REG] & MASK;
uint16_t voltage_mv = BASE + (val * STEP);
this->publish_state(voltage_mv * 0.001f); // Convert mV to V
}
};
// Template for current sensors (returns mA)
template<uint8_t REG, uint8_t MASK, uint16_t BASE, uint16_t STEP>
class CurrentSensor : public SY6970Listener, public sensor::Sensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t val = data.registers[REG] & MASK;
uint16_t current_ma = BASE + (val * STEP);
this->publish_state(current_ma);
}
};
// Specialized sensor types using templates
using SY6970VbusVoltageSensor = VoltageSensor<SY6970_REG_VBUS_VOLTAGE, 0x7F, VBUS_BASE_MV, VBUS_STEP_MV>;
using SY6970BatteryVoltageSensor = VoltageSensor<SY6970_REG_BATV, 0x7F, VBAT_BASE_MV, VBAT_STEP_MV>;
using SY6970SystemVoltageSensor = VoltageSensor<SY6970_REG_VINDPM_STATUS, 0x7F, VSYS_BASE_MV, VSYS_STEP_MV>;
using SY6970ChargeCurrentSensor = CurrentSensor<SY6970_REG_CHARGE_CURRENT_MONITOR, 0x7F, 0, CHG_CURRENT_STEP_MA>;
// Precharge current sensor needs special handling (bit shift)
class SY6970PrechargeCurrentSensor : public SY6970Listener, public sensor::Sensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t iprechg = (data.registers[SY6970_REG_PRECHARGE_CURRENT] >> 4) & 0x0F;
uint16_t iprechg_ma = PRE_CHG_BASE_MA + (iprechg * PRE_CHG_STEP_MA);
this->publish_state(iprechg_ma);
}
};
} // namespace esphome::sy6970

View File

@@ -1,201 +0,0 @@
#include "sy6970.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::sy6970 {
static const char *const TAG = "sy6970";
bool SY6970Component::read_all_registers_() {
// Read all registers from 0x00 to 0x14 in one transaction (21 bytes)
// This includes unused registers 0x0F, 0x10 for performance
if (!this->read_bytes(SY6970_REG_INPUT_CURRENT_LIMIT, this->data_.registers, 21)) {
ESP_LOGW(TAG, "Failed to read registers 0x00-0x14");
return false;
}
return true;
}
bool SY6970Component::write_register_(uint8_t reg, uint8_t value) {
if (!this->write_byte(reg, value)) {
ESP_LOGW(TAG, "Failed to write register 0x%02X", reg);
return false;
}
return true;
}
bool SY6970Component::update_register_(uint8_t reg, uint8_t mask, uint8_t value) {
uint8_t reg_value;
if (!this->read_byte(reg, &reg_value)) {
ESP_LOGW(TAG, "Failed to read register 0x%02X for update", reg);
return false;
}
reg_value = (reg_value & ~mask) | (value & mask);
return this->write_register_(reg, reg_value);
}
void SY6970Component::setup() {
ESP_LOGV(TAG, "Setting up SY6970...");
// Try to read chip ID
uint8_t reg_value;
if (!this->read_byte(SY6970_REG_DEVICE_ID, &reg_value)) {
ESP_LOGE(TAG, "Failed to communicate with SY6970");
this->mark_failed();
return;
}
uint8_t chip_id = reg_value & 0x03;
if (chip_id != 0x00) {
ESP_LOGW(TAG, "Unexpected chip ID: 0x%02X (expected 0x00)", chip_id);
}
// Apply configuration options (all have defaults now)
ESP_LOGV(TAG, "Setting LED enabled to %s", ONOFF(this->led_enabled_));
this->set_led_enabled(this->led_enabled_);
ESP_LOGV(TAG, "Setting input current limit to %u mA", this->input_current_limit_);
this->set_input_current_limit(this->input_current_limit_);
ESP_LOGV(TAG, "Setting charge voltage to %u mV", this->charge_voltage_);
this->set_charge_target_voltage(this->charge_voltage_);
ESP_LOGV(TAG, "Setting charge current to %u mA", this->charge_current_);
this->set_charge_current(this->charge_current_);
ESP_LOGV(TAG, "Setting precharge current to %u mA", this->precharge_current_);
this->set_precharge_current(this->precharge_current_);
ESP_LOGV(TAG, "Setting charge enabled to %s", ONOFF(this->charge_enabled_));
this->set_charge_enabled(this->charge_enabled_);
ESP_LOGV(TAG, "Setting ADC measurements to %s", ONOFF(this->enable_adc_));
this->set_enable_adc_measure(this->enable_adc_);
ESP_LOGV(TAG, "SY6970 initialized successfully");
}
void SY6970Component::dump_config() {
ESP_LOGCONFIG(TAG,
"SY6970:\n"
" LED Enabled: %s\n"
" Input Current Limit: %u mA\n"
" Charge Voltage: %u mV\n"
" Charge Current: %u mA\n"
" Precharge Current: %u mA\n"
" Charge Enabled: %s\n"
" ADC Enabled: %s",
ONOFF(this->led_enabled_), this->input_current_limit_, this->charge_voltage_, this->charge_current_,
this->precharge_current_, ONOFF(this->charge_enabled_), ONOFF(this->enable_adc_));
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with SY6970 failed!");
}
}
void SY6970Component::update() {
if (this->is_failed()) {
return;
}
// Read all registers in one transaction
if (!this->read_all_registers_()) {
ESP_LOGW(TAG, "Failed to read registers during update");
this->status_set_warning();
return;
}
this->status_clear_warning();
// Notify all listeners with the new data
for (auto *listener : this->listeners_) {
listener->on_data(this->data_);
}
}
void SY6970Component::set_input_current_limit(uint16_t milliamps) {
if (this->is_failed())
return;
if (milliamps < INPUT_CURRENT_MIN) {
milliamps = INPUT_CURRENT_MIN;
}
uint8_t val = (milliamps - INPUT_CURRENT_MIN) / INPUT_CURRENT_STEP;
if (val > 0x3F) {
val = 0x3F;
}
this->update_register_(SY6970_REG_INPUT_CURRENT_LIMIT, 0x3F, val);
}
void SY6970Component::set_charge_target_voltage(uint16_t millivolts) {
if (this->is_failed())
return;
if (millivolts < CHG_VOLTAGE_BASE) {
millivolts = CHG_VOLTAGE_BASE;
}
uint8_t val = (millivolts - CHG_VOLTAGE_BASE) / CHG_VOLTAGE_STEP;
if (val > 0x3F) {
val = 0x3F;
}
this->update_register_(SY6970_REG_CHARGE_VOLTAGE, 0xFC, val << 2);
}
void SY6970Component::set_precharge_current(uint16_t milliamps) {
if (this->is_failed())
return;
if (milliamps < PRE_CHG_BASE_MA) {
milliamps = PRE_CHG_BASE_MA;
}
uint8_t val = (milliamps - PRE_CHG_BASE_MA) / PRE_CHG_STEP_MA;
if (val > 0x0F) {
val = 0x0F;
}
this->update_register_(SY6970_REG_PRECHARGE_CURRENT, 0xF0, val << 4);
}
void SY6970Component::set_charge_current(uint16_t milliamps) {
if (this->is_failed())
return;
uint8_t val = milliamps / 64;
if (val > 0x7F) {
val = 0x7F;
}
this->update_register_(SY6970_REG_CHARGE_CURRENT, 0x7F, val);
}
void SY6970Component::set_charge_enabled(bool enabled) {
if (this->is_failed())
return;
this->update_register_(SY6970_REG_SYS_CONTROL, 0x10, enabled ? 0x10 : 0x00);
}
void SY6970Component::set_led_enabled(bool enabled) {
if (this->is_failed())
return;
// Bit 6: 0 = LED enabled, 1 = LED disabled
this->update_register_(SY6970_REG_TIMER_CONTROL, 0x40, enabled ? 0x00 : 0x40);
}
void SY6970Component::set_enable_adc_measure(bool enabled) {
if (this->is_failed())
return;
// Set bits to enable ADC conversion
this->update_register_(SY6970_REG_ADC_CONTROL, 0xC0, enabled ? 0xC0 : 0x00);
}
} // namespace esphome::sy6970

View File

@@ -1,122 +0,0 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include <vector>
namespace esphome::sy6970 {
// SY6970 Register addresses with descriptive names
static const uint8_t SY6970_REG_INPUT_CURRENT_LIMIT = 0x00; // Input current limit control
static const uint8_t SY6970_REG_VINDPM = 0x01; // Input voltage limit
static const uint8_t SY6970_REG_ADC_CONTROL = 0x02; // ADC control and function disable
static const uint8_t SY6970_REG_SYS_CONTROL = 0x03; // Charge enable and system config
static const uint8_t SY6970_REG_CHARGE_CURRENT = 0x04; // Fast charge current limit
static const uint8_t SY6970_REG_PRECHARGE_CURRENT = 0x05; // Pre-charge/termination current
static const uint8_t SY6970_REG_CHARGE_VOLTAGE = 0x06; // Charge voltage limit
static const uint8_t SY6970_REG_TIMER_CONTROL = 0x07; // Charge timer and status LED control
static const uint8_t SY6970_REG_IR_COMP = 0x08; // IR compensation
static const uint8_t SY6970_REG_FORCE_DPDM = 0x09; // Force DPDM detection
static const uint8_t SY6970_REG_BOOST_CONTROL = 0x0A; // Boost mode voltage/current
static const uint8_t SY6970_REG_STATUS = 0x0B; // System status (bus, charge status)
static const uint8_t SY6970_REG_FAULT = 0x0C; // Fault status (NTC)
static const uint8_t SY6970_REG_VINDPM_STATUS = 0x0D; // Input voltage limit status (also sys voltage)
static const uint8_t SY6970_REG_BATV = 0x0E; // Battery voltage
static const uint8_t SY6970_REG_VBUS_VOLTAGE = 0x11; // VBUS voltage
static const uint8_t SY6970_REG_CHARGE_CURRENT_MONITOR = 0x12; // Charge current
static const uint8_t SY6970_REG_INPUT_VOLTAGE_LIMIT = 0x13; // Input voltage limit
static const uint8_t SY6970_REG_DEVICE_ID = 0x14; // Part information
// Constants for voltage and current calculations
static const uint16_t VBUS_BASE_MV = 2600; // mV
static const uint16_t VBUS_STEP_MV = 100; // mV
static const uint16_t VBAT_BASE_MV = 2304; // mV
static const uint16_t VBAT_STEP_MV = 20; // mV
static const uint16_t VSYS_BASE_MV = 2304; // mV
static const uint16_t VSYS_STEP_MV = 20; // mV
static const uint16_t CHG_CURRENT_STEP_MA = 50; // mA
static const uint16_t PRE_CHG_BASE_MA = 64; // mA
static const uint16_t PRE_CHG_STEP_MA = 64; // mA
static const uint16_t CHG_VOLTAGE_BASE = 3840; // mV
static const uint16_t CHG_VOLTAGE_STEP = 16; // mV
static const uint16_t INPUT_CURRENT_MIN = 100; // mA
static const uint16_t INPUT_CURRENT_STEP = 50; // mA
// Bus Status values (REG_0B[7:5])
enum BusStatus {
BUS_STATUS_NO_INPUT = 0,
BUS_STATUS_USB_SDP = 1,
BUS_STATUS_USB_CDP = 2,
BUS_STATUS_USB_DCP = 3,
BUS_STATUS_HVDCP = 4,
BUS_STATUS_ADAPTER = 5,
BUS_STATUS_NO_STD_ADAPTER = 6,
BUS_STATUS_OTG = 7,
};
// Charge Status values (REG_0B[4:3])
enum ChargeStatus {
CHARGE_STATUS_NOT_CHARGING = 0,
CHARGE_STATUS_PRE_CHARGE = 1,
CHARGE_STATUS_FAST_CHARGE = 2,
CHARGE_STATUS_CHARGE_DONE = 3,
};
// Structure to hold all register data read in one transaction
struct SY6970Data {
uint8_t registers[21]; // Registers 0x00-0x14 (includes unused 0x0F, 0x10)
};
// Listener interface for components that want to receive SY6970 data updates
class SY6970Listener {
public:
virtual void on_data(const SY6970Data &data) = 0;
};
class SY6970Component : public PollingComponent, public i2c::I2CDevice {
public:
SY6970Component(bool led_enabled, uint16_t input_current_limit, uint16_t charge_voltage, uint16_t charge_current,
uint16_t precharge_current, bool charge_enabled, bool enable_adc)
: led_enabled_(led_enabled),
input_current_limit_(input_current_limit),
charge_voltage_(charge_voltage),
charge_current_(charge_current),
precharge_current_(precharge_current),
charge_enabled_(charge_enabled),
enable_adc_(enable_adc) {}
void setup() override;
void dump_config() override;
void update() override;
float get_setup_priority() const override { return setup_priority::DATA; }
// Listener registration
void add_listener(SY6970Listener *listener) { this->listeners_.push_back(listener); }
// Configuration methods to be called from lambdas
void set_input_current_limit(uint16_t milliamps);
void set_charge_target_voltage(uint16_t millivolts);
void set_precharge_current(uint16_t milliamps);
void set_charge_current(uint16_t milliamps);
void set_charge_enabled(bool enabled);
void set_led_enabled(bool enabled);
void set_enable_adc_measure(bool enabled = true);
protected:
bool read_all_registers_();
bool write_register_(uint8_t reg, uint8_t value);
bool update_register_(uint8_t reg, uint8_t mask, uint8_t value);
SY6970Data data_{};
std::vector<SY6970Listener *> listeners_;
// Configuration values to set during setup()
bool led_enabled_;
uint16_t input_current_limit_;
uint16_t charge_voltage_;
uint16_t charge_current_;
uint16_t precharge_current_;
bool charge_enabled_;
bool enable_adc_;
};
} // namespace esphome::sy6970

View File

@@ -1,52 +0,0 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns
DEPENDENCIES = ["sy6970"]
CONF_BUS_STATUS = "bus_status"
CONF_CHARGE_STATUS = "charge_status"
CONF_NTC_STATUS = "ntc_status"
SY6970BusStatusTextSensor = sy6970_ns.class_(
"SY6970BusStatusTextSensor", text_sensor.TextSensor
)
SY6970ChargeStatusTextSensor = sy6970_ns.class_(
"SY6970ChargeStatusTextSensor", text_sensor.TextSensor
)
SY6970NtcStatusTextSensor = sy6970_ns.class_(
"SY6970NtcStatusTextSensor", text_sensor.TextSensor
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component),
cv.Optional(CONF_BUS_STATUS): text_sensor.text_sensor_schema(
SY6970BusStatusTextSensor
),
cv.Optional(CONF_CHARGE_STATUS): text_sensor.text_sensor_schema(
SY6970ChargeStatusTextSensor
),
cv.Optional(CONF_NTC_STATUS): text_sensor.text_sensor_schema(
SY6970NtcStatusTextSensor
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_SY6970_ID])
if bus_status_config := config.get(CONF_BUS_STATUS):
sens = await text_sensor.new_text_sensor(bus_status_config)
cg.add(parent.add_listener(sens))
if charge_status_config := config.get(CONF_CHARGE_STATUS):
sens = await text_sensor.new_text_sensor(charge_status_config)
cg.add(parent.add_listener(sens))
if ntc_status_config := config.get(CONF_NTC_STATUS):
sens = await text_sensor.new_text_sensor(ntc_status_config)
cg.add(parent.add_listener(sens))

View File

@@ -1,96 +0,0 @@
#pragma once
#include "../sy6970.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::sy6970 {
// Bus status text sensor
class SY6970BusStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = (data.registers[SY6970_REG_STATUS] >> 5) & 0x07;
const char *status_str = this->get_bus_status_string_(status);
this->publish_state(status_str);
}
protected:
const char *get_bus_status_string_(uint8_t status) {
switch (status) {
case BUS_STATUS_NO_INPUT:
return "No Input";
case BUS_STATUS_USB_SDP:
return "USB SDP";
case BUS_STATUS_USB_CDP:
return "USB CDP";
case BUS_STATUS_USB_DCP:
return "USB DCP";
case BUS_STATUS_HVDCP:
return "HVDCP";
case BUS_STATUS_ADAPTER:
return "Adapter";
case BUS_STATUS_NO_STD_ADAPTER:
return "Non-Standard Adapter";
case BUS_STATUS_OTG:
return "OTG";
default:
return "Unknown";
}
}
};
// Charge status text sensor
class SY6970ChargeStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03;
const char *status_str = this->get_charge_status_string_(status);
this->publish_state(status_str);
}
protected:
const char *get_charge_status_string_(uint8_t status) {
switch (status) {
case CHARGE_STATUS_NOT_CHARGING:
return "Not Charging";
case CHARGE_STATUS_PRE_CHARGE:
return "Pre-charge";
case CHARGE_STATUS_FAST_CHARGE:
return "Fast Charge";
case CHARGE_STATUS_CHARGE_DONE:
return "Charge Done";
default:
return "Unknown";
}
}
};
// NTC status text sensor
class SY6970NtcStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = data.registers[SY6970_REG_FAULT] & 0x07;
const char *status_str = this->get_ntc_status_string_(status);
this->publish_state(status_str);
}
protected:
const char *get_ntc_status_string_(uint8_t status) {
switch (status) {
case 0:
return "Normal";
case 2:
return "Warm";
case 3:
return "Cool";
case 5:
return "Cold";
case 6:
return "Hot";
default:
return "Unknown";
}
}
};
} // namespace esphome::sy6970

View File

@@ -20,7 +20,7 @@ from .. import template_ns
CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_CURRENT_TEMPERATURE = "current_temperature"
TemplateWaterHeater = template_ns.class_( TemplateWaterHeater = template_ns.class_(
"TemplateWaterHeater", cg.Component, water_heater.WaterHeater "TemplateWaterHeater", water_heater.WaterHeater
) )
TemplateWaterHeaterPublishAction = template_ns.class_( TemplateWaterHeaterPublishAction = template_ns.class_(
@@ -36,29 +36,24 @@ RESTORE_MODES = {
"RESTORE_AND_CALL": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE_AND_CALL, "RESTORE_AND_CALL": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE_AND_CALL,
} }
CONFIG_SCHEMA = ( CONFIG_SCHEMA = water_heater.water_heater_schema(TemplateWaterHeater).extend(
water_heater.water_heater_schema(TemplateWaterHeater) {
.extend( cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean,
{ cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean, cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum(
cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), RESTORE_MODES, upper=True
cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum( ),
RESTORE_MODES, upper=True cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
), cv.Optional(CONF_MODE): cv.returning_lambda,
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda, cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
cv.Optional(CONF_MODE): cv.returning_lambda, water_heater.validate_water_heater_mode
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( ),
water_heater.validate_water_heater_mode }
),
}
)
.extend(cv.COMPONENT_SCHEMA)
) )
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await water_heater.register_water_heater(var, config) await water_heater.register_water_heater(var, config)
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))

View File

@@ -10,7 +10,7 @@ TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {}
void TemplateWaterHeater::setup() { void TemplateWaterHeater::setup() {
if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE || if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE ||
this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE_AND_CALL) { this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE_AND_CALL) {
auto restore = this->restore_state_(); auto restore = this->restore_state();
if (restore.has_value()) { if (restore.has_value()) {
restore->perform(); restore->perform();

View File

@@ -13,7 +13,7 @@ enum TemplateWaterHeaterRestoreMode {
WATER_HEATER_RESTORE_AND_CALL, WATER_HEATER_RESTORE_AND_CALL,
}; };
class TemplateWaterHeater : public Component, public water_heater::WaterHeater { class TemplateWaterHeater : public water_heater::WaterHeater {
public: public:
TemplateWaterHeater(); TemplateWaterHeater();

View File

@@ -1060,11 +1060,11 @@ bool ThermostatClimate::cooling_required_() {
auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature;
if (this->supports_cool_) { if (this->supports_cool_) {
if (this->current_temperature >= temperature + this->cooling_deadband_) { if (this->current_temperature > temperature + this->cooling_deadband_) {
// if the current temperature reaches or exceeds the target + deadband, cooling is required // if the current temperature exceeds the target + deadband, cooling is required
return true; return true;
} else if (this->current_temperature <= temperature - this->cooling_overrun_) { } else if (this->current_temperature < temperature - this->cooling_overrun_) {
// if the current temperature is less than or equal to the target - overrun, cooling should stop // if the current temperature is less than the target - overrun, cooling should stop
return false; return false;
} else { } else {
// if we get here, the current temperature is between target + deadband and target - overrun, // if we get here, the current temperature is between target + deadband and target - overrun,
@@ -1081,11 +1081,11 @@ bool ThermostatClimate::fanning_required_() {
if (this->supports_fan_only_) { if (this->supports_fan_only_) {
if (this->supports_fan_only_cooling_) { if (this->supports_fan_only_cooling_) {
if (this->current_temperature >= temperature + this->cooling_deadband_) { if (this->current_temperature > temperature + this->cooling_deadband_) {
// if the current temperature reaches or exceeds the target + deadband, fanning is required // if the current temperature exceeds the target + deadband, fanning is required
return true; return true;
} else if (this->current_temperature <= temperature - this->cooling_overrun_) { } else if (this->current_temperature < temperature - this->cooling_overrun_) {
// if the current temperature is less than or equal to the target - overrun, fanning should stop // if the current temperature is less than the target - overrun, fanning should stop
return false; return false;
} else { } else {
// if we get here, the current temperature is between target + deadband and target - overrun, // if we get here, the current temperature is between target + deadband and target - overrun,
@@ -1103,12 +1103,11 @@ bool ThermostatClimate::heating_required_() {
auto temperature = this->supports_two_points_ ? this->target_temperature_low : this->target_temperature; auto temperature = this->supports_two_points_ ? this->target_temperature_low : this->target_temperature;
if (this->supports_heat_) { if (this->supports_heat_) {
if (this->current_temperature <= temperature - this->heating_deadband_) { if (this->current_temperature < temperature - this->heating_deadband_) {
// if the current temperature is below or equal to the target - deadband, heating is required // if the current temperature is below the target - deadband, heating is required
return true; return true;
} else if (this->current_temperature >= temperature + this->heating_overrun_) { } else if (this->current_temperature > temperature + this->heating_overrun_) {
// if the current temperature is above or equal to the target + overrun, heating should stop // if the current temperature is above the target + overrun, heating should stop
return false; return false;
} else { } else {
// if we get here, the current temperature is between target - deadband and target + overrun, // if we get here, the current temperature is between target - deadband and target + overrun,

View File

@@ -40,9 +40,6 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
// Unsigned subtraction handles wraparound correctly, then cast to signed // Unsigned subtraction handles wraparound correctly, then cast to signed
int32_t diff = static_cast<int32_t>(epoch - static_cast<uint32_t>(current_time)); int32_t diff = static_cast<int32_t>(epoch - static_cast<uint32_t>(current_time));
if (diff >= -1 && diff <= 1) { if (diff >= -1 && diff <= 1) {
// Time is already synchronized, but still call callbacks so components
// waiting for time sync (e.g., uptime timestamp sensor) can initialize
this->time_sync_callback_.call();
return; return;
} }
} }

View File

@@ -18,7 +18,7 @@ CODEOWNERS = ["@dhoeben"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
water_heater_ns = cg.esphome_ns.namespace("water_heater") water_heater_ns = cg.esphome_ns.namespace("water_heater")
WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase) WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase, cg.Component)
WaterHeaterCall = water_heater_ns.class_("WaterHeaterCall") WaterHeaterCall = water_heater_ns.class_("WaterHeaterCall")
WaterHeaterTraits = water_heater_ns.class_("WaterHeaterTraits") WaterHeaterTraits = water_heater_ns.class_("WaterHeaterTraits")
@@ -46,7 +46,7 @@ _WATER_HEATER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
} }
), ),
} }
) ).extend(cv.COMPONENT_SCHEMA)
_WATER_HEATER_SCHEMA.add_extra(entity_duplicate_validator("water_heater")) _WATER_HEATER_SCHEMA.add_extra(entity_duplicate_validator("water_heater"))
@@ -91,6 +91,8 @@ async def register_water_heater(var: cg.Pvariable, config: ConfigType) -> cg.Pva
cg.add_define("USE_WATER_HEATER") cg.add_define("USE_WATER_HEATER")
await cg.register_component(var, config)
cg.add(cg.App.register_water_heater(var)) cg.add(cg.App.register_water_heater(var))
CORE.register_platform_component("water_heater", var) CORE.register_platform_component("water_heater", var)

View File

@@ -146,6 +146,10 @@ void WaterHeaterCall::validate_() {
} }
} }
void WaterHeater::setup() {
this->pref_ = global_preferences->make_preference<SavedWaterHeaterState>(this->get_preference_hash());
}
void WaterHeater::publish_state() { void WaterHeater::publish_state() {
auto traits = this->get_traits(); auto traits = this->get_traits();
ESP_LOGD(TAG, ESP_LOGD(TAG,
@@ -184,8 +188,7 @@ void WaterHeater::publish_state() {
this->pref_.save(&saved); this->pref_.save(&saved);
} }
optional<WaterHeaterCall> WaterHeater::restore_state_() { optional<WaterHeaterCall> WaterHeater::restore_state() {
this->pref_ = global_preferences->make_preference<SavedWaterHeaterState>(this->get_preference_hash());
SavedWaterHeaterState recovered{}; SavedWaterHeaterState recovered{};
if (!this->pref_.load(&recovered)) if (!this->pref_.load(&recovered))
return {}; return {};

View File

@@ -177,7 +177,7 @@ class WaterHeaterTraits {
WaterHeaterModeMask supported_modes_; WaterHeaterModeMask supported_modes_;
}; };
class WaterHeater : public EntityBase { class WaterHeater : public EntityBase, public Component {
public: public:
WaterHeaterMode get_mode() const { return this->mode_; } WaterHeaterMode get_mode() const { return this->mode_; }
float get_current_temperature() const { return this->current_temperature_; } float get_current_temperature() const { return this->current_temperature_; }
@@ -204,15 +204,16 @@ class WaterHeater : public EntityBase {
#endif #endif
virtual void control(const WaterHeaterCall &call) = 0; virtual void control(const WaterHeaterCall &call) = 0;
void setup() override;
optional<WaterHeaterCall> restore_state();
protected: protected:
virtual WaterHeaterTraits traits() = 0; virtual WaterHeaterTraits traits() = 0;
/// Log the traits of this water heater for dump_config(). /// Log the traits of this water heater for dump_config().
void dump_traits_(const char *tag); void dump_traits_(const char *tag);
/// Restore the state of the water heater, call this from your setup() method.
optional<WaterHeaterCall> restore_state_();
/// Set the mode of the water heater. Should only be called from control(). /// Set the mode of the water heater. Should only be called from control().
void set_mode_(WaterHeaterMode mode) { this->mode_ = mode; } void set_mode_(WaterHeaterMode mode) { this->mode_ = mode; }
/// Set the target temperature of the water heater. Should only be called from control(). /// Set the target temperature of the water heater. Should only be called from control().

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -565,11 +565,6 @@ void WiFiComponent::start() {
void WiFiComponent::restart_adapter() { void WiFiComponent::restart_adapter() {
ESP_LOGW(TAG, "Restarting adapter"); ESP_LOGW(TAG, "Restarting adapter");
this->wifi_mode_(false, {}); this->wifi_mode_(false, {});
// Clear error flag here because restart_adapter() enters COOLDOWN state,
// and check_connecting_finished() is called after cooldown without going
// through start_connecting() first. Without this clear, stale errors would
// trigger spurious "failed (callback)" logs. The canonical clear location
// is in start_connecting(); this is the only exception to that pattern.
this->error_from_callback_ = false; this->error_from_callback_ = false;
} }
@@ -623,6 +618,8 @@ void WiFiComponent::loop() {
if (!this->is_connected()) { if (!this->is_connected()) {
ESP_LOGW(TAG, "Connection lost; reconnecting"); ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
// Clear error flag before reconnecting so first attempt is not seen as immediate failure
this->error_from_callback_ = false;
this->retry_connect(); this->retry_connect();
} else { } else {
this->status_clear_warning(); this->status_clear_warning();
@@ -966,12 +963,6 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden())); ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
#endif #endif
// Clear any stale error from previous connection attempt.
// This is the canonical location for clearing the flag since all connection
// attempts go through start_connecting(). The only other clear is in
// restart_adapter() which enters COOLDOWN without calling start_connecting().
this->error_from_callback_ = false;
if (!this->wifi_sta_connect_(ap)) { if (!this->wifi_sta_connect_(ap)) {
ESP_LOGE(TAG, "wifi_sta_connect_ failed"); ESP_LOGE(TAG, "wifi_sta_connect_ failed");
// Enter cooldown to allow WiFi hardware to stabilize // Enter cooldown to allow WiFi hardware to stabilize
@@ -1077,6 +1068,7 @@ void WiFiComponent::enable() {
return; return;
ESP_LOGD(TAG, "Enabling"); ESP_LOGD(TAG, "Enabling");
this->error_from_callback_ = false;
this->state_ = WIFI_COMPONENT_STATE_OFF; this->state_ = WIFI_COMPONENT_STATE_OFF;
this->start(); this->start();
} }
@@ -1337,6 +1329,11 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
// Reset to initial phase on successful connection (don't log transition, just reset state) // Reset to initial phase on successful connection (don't log transition, just reset state)
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
this->num_retried_ = 0; this->num_retried_ = 0;
// Ensure next connection attempt does not inherit error state
// so when WiFi disconnects later we start fresh and don't see
// the first connection as a failure.
this->error_from_callback_ = false;
if (this->has_ap()) { if (this->has_ap()) {
#ifdef USE_CAPTIVE_PORTAL #ifdef USE_CAPTIVE_PORTAL
if (this->is_captive_portal_active_()) { if (this->is_captive_portal_active_()) {
@@ -1847,6 +1844,8 @@ void WiFiComponent::retry_connect() {
this->advance_to_next_target_or_increment_retry_(); this->advance_to_next_target_or_increment_retry_();
} }
this->error_from_callback_ = false;
yield(); yield();
// Check if we have a valid target before building params // Check if we have a valid target before building params
// After exhausting all networks in a phase, selected_sta_index_ may be -1 // After exhausting all networks in a phase, selected_sta_index_ may be -1
@@ -2172,6 +2171,7 @@ void WiFiComponent::process_roaming_scan_() {
this->roaming_state_ = RoamingState::CONNECTING; this->roaming_state_ = RoamingState::CONNECTING;
// Connect directly - wifi_sta_connect_ handles disconnect internally // Connect directly - wifi_sta_connect_ handles disconnect internally
this->error_from_callback_ = false;
this->start_connecting(roam_params); this->start_connecting(roam_params);
} }

View File

@@ -655,9 +655,11 @@ inline uint32_t fnv1_hash_object_id(const char *str, size_t len) {
} }
/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator). /// snprintf-like function returning std::string of maximum length \p len (excluding null terminator).
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...); std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...);
/// sprintf-like function returning std::string. /// sprintf-like function returning std::string.
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...); std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
#ifdef USE_ESP8266 #ifdef USE_ESP8266

View File

@@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["setuptools==80.10.1", "wheel>=0.43,<0.47"] requires = ["setuptools==80.10.1", "wheel>=0.43,<0.46"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]

View File

@@ -1,6 +1,6 @@
pylint==4.0.4 pylint==4.0.4
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.14.14 # also change in .pre-commit-config.yaml when updating ruff==0.14.13 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
pre-commit pre-commit

View File

@@ -692,6 +692,8 @@ HEAP_ALLOCATING_HELPERS = {
"str_truncate": "removal (function is unused)", "str_truncate": "removal (function is unused)",
"str_upper_case": "removal (function is unused)", "str_upper_case": "removal (function is unused)",
"str_snake_case": "removal (function is unused)", "str_snake_case": "removal (function is unused)",
"str_sprintf": "snprintf() with a stack buffer",
"str_snprintf": "snprintf() with a stack buffer",
} }
@@ -710,7 +712,9 @@ HEAP_ALLOCATING_HELPERS = {
r"str_sanitize(?!_)|" r"str_sanitize(?!_)|"
r"str_truncate|" r"str_truncate|"
r"str_upper_case|" r"str_upper_case|"
r"str_snake_case" r"str_snake_case|"
r"str_sprintf|"
r"str_snprintf"
r")\s*\(" + CPP_RE_EOL, r")\s*\(" + CPP_RE_EOL,
include=cpp_include, include=cpp_include,
exclude=[ exclude=[

View File

@@ -1,5 +1,5 @@
sensor: sensor:
- platform: bmp581_i2c - platform: bmp581
i2c_id: i2c_bus i2c_id: i2c_bus
temperature: temperature:
name: BMP581 Temperature name: BMP581 Temperature

View File

@@ -3,7 +3,6 @@ esp32_ble_tracker:
sensor: sensor:
- platform: bthome_mithermometer - platform: bthome_mithermometer
mac_address: A4:C1:38:4E:16:78 mac_address: A4:C1:38:4E:16:78
bindkey: eef418daf699a0c188f3bfd17e4565d9
temperature: temperature:
name: "BTHome Temperature" name: "BTHome Temperature"
humidity: humidity:

View File

@@ -1,18 +0,0 @@
remote_receiver:
id: ir_receiver
pin: ${rx_pin}
# Test various hardware types with transmitter/receiver using infrared platform
infrared:
# Infrared receiver
- platform: ir_rf_proxy
id: ir_rx
name: "IR Receiver"
remote_receiver_id: ir_receiver
# RF 900MHz receiver
- platform: ir_rf_proxy
id: rf_900_rx
name: "RF 900 Receiver"
frequency: 900 MHz
remote_receiver_id: ir_receiver

View File

@@ -1,19 +0,0 @@
remote_transmitter:
id: ir_transmitter
pin: ${tx_pin}
carrier_duty_percent: 50%
# Test various hardware types with transmitter/receiver using infrared platform
infrared:
# Infrared transmitter
- platform: ir_rf_proxy
id: ir_tx
name: "IR Transmitter"
remote_transmitter_id: ir_transmitter
# RF 433MHz transmitter
- platform: ir_rf_proxy
id: rf_433_tx
name: "RF 433 Transmitter"
frequency: 433 MHz
remote_transmitter_id: ir_transmitter

View File

@@ -1,7 +1,42 @@
network:
wifi: wifi:
ssid: MySSID ssid: MySSID
password: password1 password: password1
api: api:
remote_transmitter:
id: ir_transmitter
pin: ${tx_pin}
carrier_duty_percent: 50%
remote_receiver:
id: ir_receiver
pin: ${rx_pin}
# Test various hardware types with transmitter/receiver using infrared platform
infrared:
# Infrared transmitter
- platform: ir_rf_proxy
id: ir_tx
name: "IR Transmitter"
remote_transmitter_id: ir_transmitter
# Infrared receiver
- platform: ir_rf_proxy
id: ir_rx
name: "IR Receiver"
remote_receiver_id: ir_receiver
# RF 433MHz transmitter
- platform: ir_rf_proxy
id: rf_433_tx
name: "RF 433 Transmitter"
frequency: 433 MHz
remote_transmitter_id: ir_transmitter
# RF 900MHz receiver
- platform: ir_rf_proxy
id: rf_900_rx
name: "RF 900 Receiver"
frequency: 900 MHz
remote_receiver_id: ir_receiver

View File

@@ -1,7 +0,0 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml

View File

@@ -1,7 +0,0 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml

View File

@@ -1,7 +0,0 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml

View File

@@ -1,7 +0,0 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
tx: !include common-tx.yaml

View File

@@ -1,7 +0,0 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
tx: !include common-tx.yaml

View File

@@ -1,7 +0,0 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
tx: !include common-tx.yaml

View File

@@ -1,8 +0,0 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -2,7 +2,4 @@ substitutions:
tx_pin: GPIO4 tx_pin: GPIO4
rx_pin: GPIO5 rx_pin: GPIO5
packages: <<: !include common.yaml
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -2,7 +2,4 @@ substitutions:
tx_pin: GPIO4 tx_pin: GPIO4
rx_pin: GPIO5 rx_pin: GPIO5
packages: <<: !include common.yaml
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -2,7 +2,4 @@ substitutions:
tx_pin: GPIO4 tx_pin: GPIO4
rx_pin: GPIO5 rx_pin: GPIO5
packages: <<: !include common.yaml
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -197,9 +197,6 @@ lvgl:
- lvgl.label.update: - lvgl.label.update:
id: msgbox_label id: msgbox_label
text: Unloaded text: Unloaded
- lvgl.label.update:
id: msgbox_label
text: "" # Empty text
on_all_events: on_all_events:
logger.log: logger.log:
format: "Event %s" format: "Event %s"

View File

@@ -1,57 +0,0 @@
sy6970:
id: sy6970_component
i2c_id: i2c_bus
address: 0x6A
enable_status_led: true
input_current_limit: 1000
charge_voltage: 4200
charge_current: 500
precharge_current: 128
charge_enabled: true
enable_adc: true
update_interval: 5s
sensor:
- platform: sy6970
sy6970_id: sy6970_component
vbus_voltage:
name: "VBUS Voltage"
id: vbus_voltage_sensor
battery_voltage:
name: "Battery Voltage"
id: battery_voltage_sensor
system_voltage:
name: "System Voltage"
id: system_voltage_sensor
charge_current:
name: "Charge Current"
id: charge_current_sensor
precharge_current:
name: "Precharge Current"
id: precharge_current_sensor
binary_sensor:
- platform: sy6970
sy6970_id: sy6970_component
vbus_connected:
name: "VBUS Connected"
id: vbus_connected_binary
charging:
name: "Charging"
id: charging_binary
charge_done:
name: "Charge Done"
id: charge_done_binary
text_sensor:
- platform: sy6970
sy6970_id: sy6970_component
bus_status:
name: "Bus Status"
id: bus_status_text
charge_status:
name: "Charge Status"
id: charge_status_text
ntc_status:
name: "NTC Status"
id: ntc_status_text

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -117,7 +117,6 @@ sensor:
- 10.0 -> 12.1 - 10.0 -> 12.1
- 13.0 -> 14.0 - 13.0 -> 14.0
- clamp: - clamp:
# Infinity and NaN will be clamped (NaN -> min_value, +Infinity -> max_value, -Infinity -> min_value)
max_value: 10.0 max_value: 10.0
min_value: -10.0 min_value: -10.0
- debounce: 0.1s - debounce: 0.1s