mirror of
https://github.com/esphome/esphome.git
synced 2026-01-25 14:02:08 -07:00
Compare commits
12 Commits
rp2040_web
...
mqtt_stack
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e006216ad3 | ||
|
|
d66d05dbfc | ||
|
|
bba447e656 | ||
|
|
77b6720a25 | ||
|
|
2f8f052f43 | ||
|
|
86e70c7e76 | ||
|
|
40025bb277 | ||
|
|
438bb96687 | ||
|
|
c1e1325af2 | ||
|
|
944194e04e | ||
|
|
d27d6d64da | ||
|
|
2182d1e9f0 |
@@ -1 +1 @@
|
||||
c0db7505713f2ebf5d18f274d35469cfbdaabe1e1def9fe2195594dc345f1a49
|
||||
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce
|
||||
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -17,7 +17,7 @@ runs:
|
||||
steps:
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
|
||||
2
.github/workflows/auto-label-pr.yml
vendored
2
.github/workflows/auto-label-pr.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
|
||||
4
.github/workflows/ci-api-proto.yml
vendored
4
.github/workflows/ci-api-proto.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
4
.github/workflows/ci-docker.yml
vendored
4
.github/workflows/ci-docker.yml
vendored
@@ -43,9 +43,9 @@ jobs:
|
||||
- "docker"
|
||||
# - "lint"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Set up Docker Buildx
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Check out code from base repository
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# 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
|
||||
|
||||
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
@@ -36,13 +36,13 @@ jobs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Fetch enough history to find the merge base
|
||||
fetch-depth: 2
|
||||
@@ -237,10 +237,10 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||
steps:
|
||||
- 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
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- 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 != '[]')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -489,7 +489,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -577,7 +577,7 @@ jobs:
|
||||
version: 1.0
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -662,7 +662,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -688,7 +688,7 @@ jobs:
|
||||
skip: ${{ steps.check-script.outputs.skip }}
|
||||
steps:
|
||||
- name: Check out target branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
@@ -840,7 +840,7 @@ jobs:
|
||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
||||
steps:
|
||||
- name: Check out PR branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -908,7 +908,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -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
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Get tag
|
||||
id: tag
|
||||
# yamllint disable rule:line-length
|
||||
@@ -60,9 +60,9 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Build
|
||||
@@ -92,9 +92,9 @@ jobs:
|
||||
os: "ubuntu-24.04-arm"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
- ghcr
|
||||
- dockerhub
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
|
||||
6
.github/workflows/sync-device-classes.yml
vendored
6
.github/workflows/sync-device-classes.yml
vendored
@@ -13,16 +13,16 @@ jobs:
|
||||
if: github.repository == 'esphome/esphome'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Checkout Home Assistant
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: home-assistant/core
|
||||
path: lib/home-assistant
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.14
|
||||
rev: v0.14.13
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -88,8 +88,7 @@ esphome/components/bmp3xx/* @latonita
|
||||
esphome/components/bmp3xx_base/* @latonita @martgras
|
||||
esphome/components/bmp3xx_i2c/* @latonita
|
||||
esphome/components/bmp3xx_spi/* @latonita
|
||||
esphome/components/bmp581_base/* @danielkent-net @kahrendt
|
||||
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
|
||||
esphome/components/bmp581/* @kahrendt
|
||||
esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
@@ -482,7 +481,6 @@ esphome/components/switch/* @esphome/core
|
||||
esphome/components/switch/binary_sensor/* @ssieb
|
||||
esphome/components/sx126x/* @swoboda1337
|
||||
esphome/components/sx127x/* @swoboda1337
|
||||
esphome/components/sy6970/* @linkedupbits
|
||||
esphome/components/syslog/* @clydebarrow
|
||||
esphome/components/t6615/* @tylermenezes
|
||||
esphome/components/tc74/* @sethgirvan
|
||||
|
||||
@@ -292,7 +292,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
CONF_MAX_CONNECTIONS,
|
||||
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
|
||||
esp32=8, # 520KB RAM available
|
||||
rp2040=8, # 264KB RAM, plenty of heap available
|
||||
rp2040=4, # 264KB RAM but LWIP constraints
|
||||
bk72xx=8, # Moderate RAM
|
||||
rtl87xx=8, # Moderate RAM
|
||||
host=8, # Abundant resources
|
||||
|
||||
@@ -1844,8 +1844,23 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
|
||||
this->helper_->set_nodelay_for_message(is_log_message);
|
||||
// Toggle Nagle's algorithm based on message type to prevent log messages from
|
||||
// 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);
|
||||
if (err == APIError::WOULD_BLOCK)
|
||||
|
||||
@@ -120,39 +120,26 @@ class APIFrameHelper {
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
// Manage TCP_NODELAY (Nagle's algorithm) based on message type.
|
||||
//
|
||||
// For non-log messages (sensor data, state updates): Always disable Nagle
|
||||
// (NODELAY on) for immediate delivery - these are time-sensitive.
|
||||
//
|
||||
// For log messages: Use Nagle to coalesce multiple small log packets into
|
||||
// fewer larger packets, reducing WiFi overhead. However, we limit batching
|
||||
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
|
||||
// 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
|
||||
// buffer exhaustion and dropped messages.
|
||||
//
|
||||
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
|
||||
//
|
||||
void set_nodelay_for_message(bool is_log_message) {
|
||||
if (!is_log_message) {
|
||||
if (this->nodelay_state_ != NODELAY_ON) {
|
||||
this->set_nodelay_raw_(true);
|
||||
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_++;
|
||||
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
|
||||
///
|
||||
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
|
||||
/// state updates low-latency (NODELAY enabled). Without this, many small log
|
||||
/// packets fill the TCP send buffer, crowding out important state updates.
|
||||
///
|
||||
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
|
||||
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
|
||||
///
|
||||
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
|
||||
/// @return true if successful or already in desired state
|
||||
bool set_nodelay(bool enable) {
|
||||
if (this->nodelay_enabled_ == enable)
|
||||
return true;
|
||||
int val = enable ? 1 : 0;
|
||||
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
if (err == 0) {
|
||||
this->nodelay_enabled_ = enable;
|
||||
}
|
||||
return err == 0;
|
||||
}
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf messages in a single operation
|
||||
@@ -242,18 +229,10 @@ class APIFrameHelper {
|
||||
uint8_t tx_buf_head_{0};
|
||||
uint8_t tx_buf_tail_{0};
|
||||
uint8_t tx_buf_count_{0};
|
||||
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
|
||||
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
|
||||
static constexpr int8_t NODELAY_ON = -1;
|
||||
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));
|
||||
}
|
||||
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
|
||||
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
|
||||
// messages to coalesce while keeping state updates low-latency.
|
||||
bool nodelay_enabled_{true};
|
||||
|
||||
// Common initialization for both plaintext and noise protocols
|
||||
APIError init_common_();
|
||||
|
||||
@@ -13,11 +13,14 @@ from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
|
||||
CODEOWNERS = ["@jasstrong"]
|
||||
DEPENDENCIES = ["sensor"]
|
||||
|
||||
UNIT_INDEX = "index"
|
||||
|
||||
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
AQISensor,
|
||||
unit_of_measurement=UNIT_INDEX,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
|
||||
@@ -38,10 +38,8 @@ async def to_code(config):
|
||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||
elif CORE.is_rp2040:
|
||||
# https://github.com/ayushsharma82/RPAsyncTCP
|
||||
# RPAsyncTCP is a drop-in replacement for AsyncTCP_RP2040W with better
|
||||
# ESPAsyncWebServer compatibility
|
||||
cg.add_library("ayushsharma82/RPAsyncTCP", "1.3.2")
|
||||
# https://github.com/khoih-prog/AsyncTCP_RP2040W
|
||||
cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0")
|
||||
# Other platforms (host, etc) use socket-based implementation
|
||||
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
|
||||
#include <ESPAsyncTCP.h>
|
||||
#elif defined(USE_RP2040)
|
||||
// Use RPAsyncTCP library for RP2040
|
||||
#include <RPAsyncTCP.h>
|
||||
// Use AsyncTCP_RP2040W library for RP2040
|
||||
#include <AsyncTCP_RP2040W.h>
|
||||
#else
|
||||
// Use socket-based implementation for other platforms
|
||||
#include "async_tcp_socket.h"
|
||||
|
||||
@@ -108,14 +108,10 @@ void ATM90E32Component::update() {
|
||||
#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() {
|
||||
this->spi_setup();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
this->cs_summary_ = this->cs_->dump_summary();
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
|
||||
uint16_t mmode0 = 0x87; // 3P4W 50Hz
|
||||
uint16_t high_thresh = 0;
|
||||
@@ -163,13 +159,13 @@ void ATM90E32Component::setup() {
|
||||
if (this->enable_offset_calibration_) {
|
||||
// Initialize flash storage for offset calibrations
|
||||
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->restore_offset_calibrations_();
|
||||
|
||||
// Initialize flash storage for power offset calibrations
|
||||
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->restore_power_offset_calibrations_();
|
||||
} else {
|
||||
@@ -190,7 +186,7 @@ void ATM90E32Component::setup() {
|
||||
if (this->enable_gain_calibration_) {
|
||||
// Initialize flash storage for 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->restore_gain_calibrations_();
|
||||
|
||||
@@ -221,8 +217,7 @@ void ATM90E32Component::setup() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::log_calibration_status_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
|
||||
bool offset_mismatch = false;
|
||||
bool power_mismatch = false;
|
||||
@@ -573,8 +568,7 @@ float ATM90E32Component::get_chip_temperature_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_gain_calibrations() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->enable_gain_calibration_) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
|
||||
cs);
|
||||
@@ -674,8 +668,7 @@ void ATM90E32Component::run_gain_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_gain_calibration_to_memory_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
@@ -688,8 +681,7 @@ void ATM90E32Component::save_gain_calibration_to_memory_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_offset_calibration_to_memory_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
bool success = this->offset_pref_.save(&this->offset_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
@@ -705,8 +697,7 @@ void ATM90E32Component::save_offset_calibration_to_memory_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_power_offset_calibration_to_memory_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
@@ -722,8 +713,7 @@ void ATM90E32Component::save_power_offset_calibration_to_memory_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_offset_calibrations() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGW(TAG,
|
||||
"[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() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGW(
|
||||
TAG,
|
||||
@@ -827,8 +816,7 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_gain_calibrations_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
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].current_gain = this->phase_[i].ct_gain_;
|
||||
@@ -882,8 +870,7 @@ void ATM90E32Component::restore_gain_calibrations_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_offset_calibrations_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
for (uint8_t i = 0; i < 3; ++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_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
for (uint8_t i = 0; i < 3; ++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() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->using_saved_calibrations_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
|
||||
@@ -1018,8 +1003,7 @@ void ATM90E32Component::clear_gain_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_offset_calibrations() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->restored_offset_calibration_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
||||
@@ -1061,8 +1045,7 @@ void ATM90E32Component::clear_offset_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_power_offset_calibrations() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
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] ---------------------------------------------------------------------", cs);
|
||||
@@ -1137,8 +1120,7 @@ int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive)
|
||||
}
|
||||
|
||||
bool ATM90E32Component::verify_gain_writes_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
bool success = true;
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#include "atm90e32_reg.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
@@ -184,7 +182,6 @@ class ATM90E32Component : public PollingComponent,
|
||||
bool verify_gain_writes_();
|
||||
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
|
||||
void log_calibration_status_();
|
||||
void get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer);
|
||||
|
||||
struct ATM90E32Phase {
|
||||
uint16_t voltage_gain_{0};
|
||||
@@ -250,6 +247,7 @@ class ATM90E32Component : public PollingComponent,
|
||||
ESPPreferenceObject offset_pref_;
|
||||
ESPPreferenceObject power_offset_pref_;
|
||||
ESPPreferenceObject gain_calibration_pref_;
|
||||
std::string cs_summary_;
|
||||
|
||||
sensor::Sensor *freq_sensor_{nullptr};
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
* - 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/hal.h"
|
||||
|
||||
namespace esphome::bmp581_base {
|
||||
namespace esphome {
|
||||
namespace bmp581 {
|
||||
|
||||
static const char *const TAG = "bmp581";
|
||||
|
||||
@@ -90,6 +91,7 @@ void BMP581Component::dump_config() {
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
|
||||
@@ -147,7 +149,7 @@ void BMP581Component::setup() {
|
||||
uint8_t chip_id;
|
||||
|
||||
// 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");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
@@ -170,7 +172,7 @@ void BMP581Component::setup() {
|
||||
// 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");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
@@ -357,7 +359,7 @@ bool BMP581Component::check_data_readiness_() {
|
||||
|
||||
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");
|
||||
return false;
|
||||
}
|
||||
@@ -398,7 +400,7 @@ bool BMP581Component::prime_iir_filter_() {
|
||||
|
||||
// flush the IIR filter with forced measurements (we will only flush once)
|
||||
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");
|
||||
|
||||
return false;
|
||||
@@ -428,7 +430,7 @@ bool BMP581Component::prime_iir_filter_() {
|
||||
|
||||
// disable IIR filter flushings on future forced measurements
|
||||
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");
|
||||
|
||||
return false;
|
||||
@@ -452,7 +454,7 @@ bool BMP581Component::read_temperature_(float &temperature) {
|
||||
}
|
||||
|
||||
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");
|
||||
this->status_set_warning();
|
||||
|
||||
@@ -481,7 +483,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &
|
||||
}
|
||||
|
||||
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");
|
||||
this->status_set_warning();
|
||||
|
||||
@@ -505,7 +507,7 @@ bool BMP581Component::reset_() {
|
||||
// - returns the Power-On-Reboot interrupt status, which is asserted if successful
|
||||
|
||||
// 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");
|
||||
|
||||
return false;
|
||||
@@ -516,7 +518,7 @@ bool BMP581Component::reset_() {
|
||||
delay(3);
|
||||
|
||||
// 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");
|
||||
|
||||
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
|
||||
// - 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};
|
||||
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) {
|
||||
@@ -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;
|
||||
|
||||
// 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,
|
||||
@@ -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_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) {
|
||||
@@ -591,7 +593,8 @@ bool BMP581Component::write_power_mode_(OperationMode mode) {
|
||||
this->odr_config_.bit.pwr_mode = mode;
|
||||
|
||||
// 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
|
||||
@@ -3,9 +3,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.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 RESET_COMMAND = 0xB6; // Soft reset command
|
||||
@@ -57,7 +59,7 @@ enum IIRFilter {
|
||||
IIR_FILTER_128 = 0x7
|
||||
};
|
||||
|
||||
class BMP581Component : public PollingComponent {
|
||||
class BMP581Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
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; }
|
||||
|
||||
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 *pressure_sensor_{nullptr};
|
||||
|
||||
@@ -219,4 +216,5 @@ class BMP581Component : public PollingComponent {
|
||||
} odr_config_ = {.reg = 0};
|
||||
};
|
||||
|
||||
} // namespace esphome::bmp581_base
|
||||
} // namespace bmp581
|
||||
} // namespace esphome
|
||||
@@ -1,5 +1,164 @@
|
||||
import esphome.config_validation as cv
|
||||
import math
|
||||
|
||||
CONFIG_SCHEMA = cv.invalid(
|
||||
"The bmp581 sensor component has been renamed to bmp581_i2c."
|
||||
import esphome.codegen as cg
|
||||
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)))
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -1,8 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble_tracker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BINDKEY, CONF_ID, CONF_MAC_ADDRESS
|
||||
from esphome.core import HexInt
|
||||
from esphome.const import CONF_ID, CONF_MAC_ADDRESS
|
||||
|
||||
CODEOWNERS = ["@nagyrobi"]
|
||||
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.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_BINDKEY): cv.bind_key,
|
||||
}
|
||||
)
|
||||
.extend(BLE_DEVICE_SCHEMA)
|
||||
@@ -36,9 +34,3 @@ async def setup_bthome_mithermometer(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await esp32_ble_tracker.register_ble_device(var, config)
|
||||
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)))
|
||||
|
||||
@@ -3,23 +3,15 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "mbedtls/ccm.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace 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) {
|
||||
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
||||
@@ -138,10 +130,6 @@ void BTHomeMiThermometer::dump_config() {
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
|
||||
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(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
@@ -162,60 +150,6 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev
|
||||
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,
|
||||
const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
if (!service_data.uuid.contains(0xD2, 0xFC)) {
|
||||
@@ -239,88 +173,51 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t source_address = device.address_uint64();
|
||||
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;
|
||||
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
if (is_encrypted) {
|
||||
if (!this->decrypt_bthome_payload_(data, source_address, decrypted_payload)) {
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
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;
|
||||
ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf));
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t payload_index = 1;
|
||||
uint64_t source_address = device.address_uint64();
|
||||
|
||||
if (mac_included) {
|
||||
if (payload_size < 6) {
|
||||
if (data.size() < 7) {
|
||||
ESP_LOGVV(TAG, "BTHome payload missing MAC address");
|
||||
return false;
|
||||
}
|
||||
source_address = 0;
|
||||
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_size -= 6;
|
||||
payload_index = 7;
|
||||
}
|
||||
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
if (source_address != this->address_) {
|
||||
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload_size == 0) {
|
||||
if (payload_index >= data.size()) {
|
||||
ESP_LOGVV(TAG, "BTHome payload empty after header");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool reported = false;
|
||||
size_t offset = 0;
|
||||
size_t offset = payload_index;
|
||||
uint8_t last_type = 0;
|
||||
|
||||
while (offset < payload_size) {
|
||||
const uint8_t obj_type = payload[offset++];
|
||||
while (offset < data.size()) {
|
||||
const uint8_t obj_type = data[offset++];
|
||||
size_t value_length = 0;
|
||||
bool has_length_byte = obj_type == 0x53; // text objects include explicit length
|
||||
|
||||
if (has_length_byte) {
|
||||
if (offset >= payload_size) {
|
||||
if (offset >= data.size()) {
|
||||
break;
|
||||
}
|
||||
value_length = payload[offset++];
|
||||
value_length = data[offset++];
|
||||
} else {
|
||||
if (!get_bthome_value_length(obj_type, value_length)) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (offset + value_length > payload_size) {
|
||||
if (offset + value_length > data.size()) {
|
||||
ESP_LOGVV(TAG, "BTHome object length exceeds payload");
|
||||
break;
|
||||
}
|
||||
|
||||
const uint8_t *value = &payload[offset];
|
||||
const uint8_t *value = &data[offset];
|
||||
offset += value_length;
|
||||
|
||||
if (obj_type < last_type) {
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -16,7 +14,6 @@ namespace bthome_mithermometer {
|
||||
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
public:
|
||||
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_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
||||
@@ -30,13 +27,9 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi
|
||||
protected:
|
||||
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||
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};
|
||||
optional<uint8_t> last_packet_id_{};
|
||||
bool has_bindkey_{false};
|
||||
uint8_t bindkey_[16];
|
||||
|
||||
sensor::Sensor *temperature_{nullptr};
|
||||
sensor::Sensor *humidity_{nullptr};
|
||||
|
||||
@@ -13,7 +13,6 @@ from esphome.const import (
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_RTL87XX,
|
||||
PlatformFramework,
|
||||
)
|
||||
@@ -54,7 +53,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_RTL87XX,
|
||||
]
|
||||
),
|
||||
@@ -108,8 +106,6 @@ async def to_code(config):
|
||||
cg.add_library("DNSServer", None)
|
||||
if CORE.is_libretiny:
|
||||
cg.add_library("DNSServer", None)
|
||||
if CORE.is_rp2040:
|
||||
cg.add_library("DNSServer", None)
|
||||
|
||||
|
||||
# Only compile the ESP-IDF DNS server when using ESP-IDF framework
|
||||
|
||||
@@ -89,8 +89,10 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
delayMicroseconds(500);
|
||||
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
|
||||
delayMicroseconds(2000);
|
||||
} else {
|
||||
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
|
||||
delayMicroseconds(1000);
|
||||
} else {
|
||||
delayMicroseconds(800);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#endif
|
||||
@@ -185,23 +184,15 @@ bool Esp32HostedUpdate::fetch_manifest_() {
|
||||
}
|
||||
|
||||
// Read manifest JSON into string (manifest is small, ~1KB max)
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
std::string json_str;
|
||||
json_str.reserve(container->content_length);
|
||||
uint8_t buf[256];
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read_or_error = container->read(buf, sizeof(buf));
|
||||
App.feed_wdt();
|
||||
int read = container->read(buf, sizeof(buf));
|
||||
if (read > 0) {
|
||||
json_str.append(reinterpret_cast<char *>(buf), read);
|
||||
}
|
||||
yield();
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != http_request::HttpReadLoopResult::DATA)
|
||||
break; // ERROR or TIMEOUT
|
||||
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
|
||||
}
|
||||
container->end();
|
||||
|
||||
@@ -306,38 +297,32 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
|
||||
}
|
||||
|
||||
// Stream firmware to coprocessor while computing SHA256
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
sha256::SHA256 hasher;
|
||||
hasher.init();
|
||||
|
||||
uint8_t buffer[CHUNK_SIZE];
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
|
||||
while (container->get_bytes_read() < total_size) {
|
||||
int read_or_error = container->read(buffer, sizeof(buffer));
|
||||
int read = container->read(buffer, sizeof(buffer));
|
||||
|
||||
// Feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != http_request::HttpReadLoopResult::DATA) {
|
||||
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading firmware data");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (read <= 0) {
|
||||
if (read < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Download failed"));
|
||||
return false;
|
||||
}
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Download failed"));
|
||||
return false;
|
||||
// read == 0: no more data available, exit loop
|
||||
break;
|
||||
}
|
||||
|
||||
hasher.add(buffer, read_or_error);
|
||||
err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
|
||||
hasher.add(buffer, read);
|
||||
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "fingerprint_grow.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -533,21 +532,14 @@ void FingerprintGrowComponent::sensor_sleep_() {
|
||||
}
|
||||
|
||||
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,
|
||||
"GROW_FINGERPRINT_READER:\n"
|
||||
" System Identifier Code: 0x%.4X\n"
|
||||
" Touch Sensing Pin: %s\n"
|
||||
" Sensor Power Pin: %s",
|
||||
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
|
||||
this->has_power_pin_ ? power_pin_buf : "None");
|
||||
this->system_identifier_code_,
|
||||
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) {
|
||||
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
|
||||
} else {
|
||||
|
||||
@@ -3,44 +3,13 @@
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include <map>
|
||||
#include <IRSender.h>
|
||||
#include <HeatpumpIRFactory.h>
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "ir_sender_esphome.h"
|
||||
#include "HeatpumpIRFactory.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
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";
|
||||
|
||||
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {
|
||||
|
||||
32
esphome/components/heatpumpir/ir_sender_esphome.cpp
Normal file
32
esphome/components/heatpumpir/ir_sender_esphome.cpp
Normal 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
|
||||
25
esphome/components/heatpumpir/ir_sender_esphome.h
Normal file
25
esphome/components/heatpumpir/ir_sender_esphome.h
Normal 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
|
||||
@@ -79,81 +79,6 @@ inline bool is_redirect(int const status) {
|
||||
*/
|
||||
inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
|
||||
|
||||
/*
|
||||
* HTTP Container Read Semantics
|
||||
* =============================
|
||||
*
|
||||
* IMPORTANT: These semantics differ from standard BSD sockets!
|
||||
*
|
||||
* BSD socket read() returns:
|
||||
* > 0: bytes read
|
||||
* == 0: connection closed (EOF)
|
||||
* < 0: error (check errno)
|
||||
*
|
||||
* HttpContainer::read() returns:
|
||||
* > 0: bytes read successfully
|
||||
* == 0: no data available yet OR all content read
|
||||
* (caller should check bytes_read vs content_length)
|
||||
* < 0: error or connection closed (caller should EXIT)
|
||||
* HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
|
||||
* other negative values = platform-specific errors
|
||||
*
|
||||
* Platform behaviors:
|
||||
* - ESP-IDF: blocking reads, 0 only returned when all content read
|
||||
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
|
||||
*
|
||||
* Use the helper functions below instead of checking return values directly:
|
||||
* - http_read_loop_result(): for manual loops with per-chunk processing
|
||||
* - http_read_fully(): for simple "read N bytes into buffer" operations
|
||||
*/
|
||||
|
||||
/// Error code returned by HttpContainer::read() when connection closed prematurely
|
||||
/// NOTE: Unlike BSD sockets where 0 means EOF, here 0 means "no data yet, retry"
|
||||
static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1;
|
||||
|
||||
/// Status of a read operation
|
||||
enum class HttpReadStatus : uint8_t {
|
||||
OK, ///< Read completed successfully
|
||||
ERROR, ///< Read error occurred
|
||||
TIMEOUT, ///< Timeout waiting for data
|
||||
};
|
||||
|
||||
/// Result of an HTTP read operation
|
||||
struct HttpReadResult {
|
||||
HttpReadStatus status; ///< Status of the read operation
|
||||
int error_code; ///< Error code from read() on failure, 0 on success
|
||||
};
|
||||
|
||||
/// Result of processing a non-blocking read with timeout (for manual loops)
|
||||
enum class HttpReadLoopResult : uint8_t {
|
||||
DATA, ///< Data was read, process it
|
||||
RETRY, ///< No data yet, already delayed, caller should continue loop
|
||||
ERROR, ///< Read error, caller should exit loop
|
||||
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
||||
};
|
||||
|
||||
/// Process a read result with timeout tracking and delay handling
|
||||
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
|
||||
/// @param last_data_time Time of last successful read, updated when data received
|
||||
/// @param timeout_ms Maximum time to wait for data
|
||||
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
|
||||
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
|
||||
uint32_t timeout_ms) {
|
||||
if (bytes_read_or_error > 0) {
|
||||
last_data_time = millis();
|
||||
return HttpReadLoopResult::DATA;
|
||||
}
|
||||
if (bytes_read_or_error < 0) {
|
||||
return HttpReadLoopResult::ERROR;
|
||||
}
|
||||
// bytes_read_or_error == 0: no data available yet
|
||||
if (millis() - last_data_time >= timeout_ms) {
|
||||
return HttpReadLoopResult::TIMEOUT;
|
||||
}
|
||||
delay(1); // Small delay to prevent tight spinning
|
||||
return HttpReadLoopResult::RETRY;
|
||||
}
|
||||
|
||||
class HttpRequestComponent;
|
||||
|
||||
class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
@@ -163,33 +88,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
int status_code;
|
||||
uint32_t duration_ms;
|
||||
|
||||
/**
|
||||
* @brief Read data from the HTTP response body.
|
||||
*
|
||||
* WARNING: These semantics differ from BSD sockets!
|
||||
* BSD sockets: 0 = EOF (connection closed)
|
||||
* This method: 0 = no data yet OR all content read, negative = error/closed
|
||||
*
|
||||
* @param buf Buffer to read data into
|
||||
* @param max_len Maximum number of bytes to read
|
||||
* @return
|
||||
* - > 0: Number of bytes read successfully
|
||||
* - 0: No data available yet OR all content read
|
||||
* (check get_bytes_read() >= content_length to distinguish)
|
||||
* - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely
|
||||
* - < -1: Other error (platform-specific error code)
|
||||
*
|
||||
* Platform notes:
|
||||
* - ESP-IDF: blocking read, 0 only when all content read
|
||||
* - Arduino: non-blocking, 0 can mean "no data yet" or "all content read"
|
||||
*
|
||||
* Use get_bytes_read() and content_length to track progress.
|
||||
* When get_bytes_read() >= content_length, all data has been received.
|
||||
*
|
||||
* IMPORTANT: Do not use raw return values directly. Use these helpers:
|
||||
* - http_read_loop_result(): for loops with per-chunk processing
|
||||
* - http_read_fully(): for simple "read N bytes" operations
|
||||
*/
|
||||
virtual int read(uint8_t *buf, size_t max_len) = 0;
|
||||
virtual void end() = 0;
|
||||
|
||||
@@ -212,38 +110,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
std::map<std::string, std::list<std::string>> response_headers_{};
|
||||
};
|
||||
|
||||
/// Read data from HTTP container into buffer with timeout handling
|
||||
/// Handles feed_wdt, yield, and timeout checking internally
|
||||
/// @param container The HTTP container to read from
|
||||
/// @param buffer Buffer to read into
|
||||
/// @param total_size Total bytes to read
|
||||
/// @param chunk_size Maximum bytes per read call
|
||||
/// @param timeout_ms Read timeout in milliseconds
|
||||
/// @return HttpReadResult with status and error_code on failure
|
||||
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
|
||||
uint32_t timeout_ms) {
|
||||
size_t read_index = 0;
|
||||
uint32_t last_data_time = millis();
|
||||
|
||||
while (read_index < total_size) {
|
||||
int read_bytes_or_error = container->read(buffer + read_index, std::min(chunk_size, total_size - read_index));
|
||||
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result == HttpReadLoopResult::ERROR)
|
||||
return {HttpReadStatus::ERROR, read_bytes_or_error};
|
||||
if (result == HttpReadLoopResult::TIMEOUT)
|
||||
return {HttpReadStatus::TIMEOUT, 0};
|
||||
|
||||
read_index += read_bytes_or_error;
|
||||
}
|
||||
return {HttpReadStatus::OK, 0};
|
||||
}
|
||||
|
||||
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
|
||||
public:
|
||||
void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
|
||||
@@ -258,7 +124,6 @@ class HttpRequestComponent : public Component {
|
||||
|
||||
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
|
||||
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
uint32_t get_timeout() const { return this->timeout_; }
|
||||
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
|
||||
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
|
||||
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
|
||||
@@ -384,21 +249,15 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *buf = allocator.allocate(max_length);
|
||||
if (buf != nullptr) {
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
size_t read_index = 0;
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->parent_->get_timeout();
|
||||
while (container->get_bytes_read() < max_length) {
|
||||
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
if (read <= 0) {
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA)
|
||||
break; // ERROR or TIMEOUT
|
||||
read_index += read_or_error;
|
||||
read_index += read;
|
||||
}
|
||||
response_body.reserve(read_index);
|
||||
response_body.assign((char *) buf, read_index);
|
||||
|
||||
@@ -139,23 +139,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
return container;
|
||||
}
|
||||
|
||||
// Arduino HTTP read implementation
|
||||
//
|
||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||
//
|
||||
// Arduino's WiFiClient is inherently non-blocking - available() returns 0 when
|
||||
// no data is ready. We use connected() to distinguish "no data yet" from
|
||||
// "connection closed".
|
||||
//
|
||||
// WiFiClient behavior:
|
||||
// available() > 0: data ready to read
|
||||
// available() == 0 && connected(): no data yet, still connected
|
||||
// available() == 0 && !connected(): connection closed
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
|
||||
// > 0: bytes read
|
||||
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
|
||||
// < 0: error/connection closed <-- connection closed returns -1, not 0
|
||||
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
@@ -163,7 +146,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
WiFiClient *stream_ptr = this->client_.getStreamPtr();
|
||||
if (stream_ptr == nullptr) {
|
||||
ESP_LOGE(TAG, "Stream pointer vanished!");
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int available_data = stream_ptr->available();
|
||||
@@ -171,15 +154,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
// Check if we've read all expected content
|
||||
if (this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
// No data available - check if connection is still open
|
||||
if (!stream_ptr->connected()) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
|
||||
}
|
||||
return 0; // No data yet, caller should retry
|
||||
return 0;
|
||||
}
|
||||
|
||||
App.feed_wdt();
|
||||
|
||||
@@ -209,57 +209,26 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
return container;
|
||||
}
|
||||
|
||||
// ESP-IDF HTTP read implementation (blocking mode)
|
||||
//
|
||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||
//
|
||||
// esp_http_client_read() in blocking mode returns:
|
||||
// > 0: bytes read
|
||||
// 0: connection closed (end of stream)
|
||||
// < 0: error
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract:
|
||||
// > 0: bytes read
|
||||
// 0: no data yet / all content read (caller should check bytes_read vs content_length)
|
||||
// < 0: error/connection closed
|
||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
// Check if we've already read all expected content
|
||||
if (this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
this->feed_wdt();
|
||||
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len);
|
||||
this->feed_wdt();
|
||||
if (read_len > 0) {
|
||||
this->bytes_read_ += read_len;
|
||||
}
|
||||
|
||||
this->feed_wdt();
|
||||
int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
|
||||
this->feed_wdt();
|
||||
|
||||
this->duration_ms += (millis() - start);
|
||||
|
||||
if (read_len_or_error > 0) {
|
||||
this->bytes_read_ += read_len_or_error;
|
||||
return read_len_or_error;
|
||||
}
|
||||
|
||||
// Connection closed by server before all content received
|
||||
if (read_len_or_error == 0) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
// Negative value - error, return the actual error code for debugging
|
||||
return read_len_or_error;
|
||||
return read_len;
|
||||
}
|
||||
|
||||
void HttpContainerIDF::end() {
|
||||
if (this->client_ == nullptr) {
|
||||
return; // Already cleaned up
|
||||
}
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
esp_http_client_close(this->client_);
|
||||
esp_http_client_cleanup(this->client_);
|
||||
this->client_ = nullptr;
|
||||
}
|
||||
|
||||
void HttpContainerIDF::feed_wdt() {
|
||||
|
||||
@@ -115,47 +115,39 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
return error_code;
|
||||
}
|
||||
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->parent_->get_timeout();
|
||||
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
// read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
|
||||
int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
|
||||
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
|
||||
container->content_length, bufsize_or_error);
|
||||
// read a maximum of chunk_size bytes into buf. (real read size returned)
|
||||
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
|
||||
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
|
||||
container->content_length, bufsize);
|
||||
|
||||
// feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA) {
|
||||
if (result == HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading data");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (bufsize <= 0) {
|
||||
if (bufsize < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
}
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
// bufsize == 0: no more data available, exit loop
|
||||
break;
|
||||
}
|
||||
|
||||
// At this point bufsize_or_error > 0, so it's a valid size
|
||||
if (bufsize_or_error <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// add read bytes to MD5
|
||||
md5_receive.add(buf, bufsize_or_error);
|
||||
md5_receive.add(buf, bufsize);
|
||||
|
||||
// write bytes to OTA backend
|
||||
this->update_started_ = true;
|
||||
error_code = backend->write(buf, bufsize_or_error);
|
||||
error_code = backend->write(buf, bufsize);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
// error code explanation available at
|
||||
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
|
||||
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
|
||||
container->get_bytes_read() - bufsize_or_error, container->content_length);
|
||||
container->get_bytes_read() - bufsize, container->content_length);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return error_code;
|
||||
}
|
||||
@@ -252,19 +244,19 @@ bool OtaHttpRequestComponent::http_get_md5_() {
|
||||
}
|
||||
|
||||
this->md5_expected_.resize(MD5_SIZE);
|
||||
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
|
||||
this->parent_->get_timeout());
|
||||
int read_len = 0;
|
||||
while (container->get_bytes_read() < MD5_SIZE) {
|
||||
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
|
||||
if (read_len <= 0) {
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
}
|
||||
container->end();
|
||||
|
||||
if (result.status != HttpReadStatus::OK) {
|
||||
if (result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading MD5");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading MD5: %d", result.error_code);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
|
||||
return read_len == MD5_SIZE;
|
||||
}
|
||||
|
||||
bool OtaHttpRequestComponent::validate_url_(const std::string &url) {
|
||||
|
||||
@@ -11,12 +11,7 @@ namespace http_request {
|
||||
|
||||
// The update function runs in a task only on ESP32s.
|
||||
#ifdef USE_ESP32
|
||||
// vTaskDelete doesn't return, but clang-tidy doesn't know that
|
||||
#define UPDATE_RETURN \
|
||||
do { \
|
||||
vTaskDelete(nullptr); \
|
||||
__builtin_unreachable(); \
|
||||
} while (0)
|
||||
#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
|
||||
#else
|
||||
#define UPDATE_RETURN return
|
||||
#endif
|
||||
@@ -75,21 +70,19 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
|
||||
this_update->request_parent_->get_timeout());
|
||||
if (read_result.status != HttpReadStatus::OK) {
|
||||
if (read_result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading manifest");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
|
||||
size_t read_index = 0;
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
|
||||
|
||||
yield();
|
||||
|
||||
if (read_bytes <= 0) {
|
||||
// Network error or connection closed - break to avoid infinite loop
|
||||
break;
|
||||
}
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
|
||||
allocator.deallocate(data, container->content_length);
|
||||
container->end();
|
||||
UPDATE_RETURN;
|
||||
|
||||
read_index += read_bytes;
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
|
||||
bool valid = false;
|
||||
{ // Ensures the response string falls out of scope and deallocates before the task ends
|
||||
|
||||
@@ -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;
|
||||
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) {
|
||||
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Once the API is considered stable, this warning will be removed.
|
||||
|
||||
#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 {
|
||||
|
||||
|
||||
@@ -391,10 +391,7 @@ void LightCall::transform_parameters_() {
|
||||
min_mireds > 0.0f && max_mireds > 0.0f) {
|
||||
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
|
||||
this->parent_->get_name().c_str());
|
||||
// Only compute cold_white/warm_white from color_temperature if they're not already explicitly set.
|
||||
// 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()) {
|
||||
if (this->has_color_temperature()) {
|
||||
const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds);
|
||||
const float range = max_mireds - min_mireds;
|
||||
const float ww_fraction = (color_temp - min_mireds) / range;
|
||||
|
||||
@@ -32,7 +32,7 @@ class LabelType(WidgetType):
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
"""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_LONG_MODE, config)
|
||||
await w.set_property(CONF_RECOLOR, config)
|
||||
|
||||
@@ -55,44 +55,3 @@ DriverChip(
|
||||
(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),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "mipi_rgb.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
#include <span>
|
||||
|
||||
namespace esphome {
|
||||
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)
|
||||
return "None";
|
||||
pin->dump_summary(buffer.data(), buffer.size());
|
||||
return buffer.data();
|
||||
return pin->dump_summary();
|
||||
}
|
||||
|
||||
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++) {
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, pin_summary);
|
||||
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
"MIPI_RGB LCD"
|
||||
"\n Model: %s"
|
||||
@@ -389,9 +379,9 @@ void MipiRgb::dump_config() {
|
||||
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->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_),
|
||||
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_, reset_buf),
|
||||
get_pin_name(this->de_pin_, de_buf), get_pin_name(this->pclk_pin_, pclk_buf),
|
||||
get_pin_name(this->hsync_pin_, hsync_buf), get_pin_name(this->vsync_pin_, vsync_buf));
|
||||
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(),
|
||||
get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
|
||||
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_(13, 16, "Green", 0);
|
||||
|
||||
@@ -55,7 +55,6 @@ st7701s = ST7701S(
|
||||
pclk_frequency="16MHz",
|
||||
pclk_inverted=True,
|
||||
initsequence=(
|
||||
(0x01,), # Software Reset
|
||||
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0
|
||||
(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,),
|
||||
|
||||
@@ -279,7 +279,7 @@ def modbus_calc_properties(config):
|
||||
if isinstance(value, str):
|
||||
value = value.encode()
|
||||
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
|
||||
return byte_offset, reg_count
|
||||
|
||||
|
||||
@@ -119,7 +119,8 @@ bool MQTTAlarmControlPanelComponent::publish_state() {
|
||||
default:
|
||||
state_s = "unknown";
|
||||
}
|
||||
return this->publish(this->get_state_topic_(), state_s);
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -52,8 +52,9 @@ bool MQTTBinarySensorComponent::publish_state(bool state) {
|
||||
if (this->binary_sensor_->is_status_binary_sensor())
|
||||
return true;
|
||||
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
const char *state_s = state ? "ON" : "OFF";
|
||||
return this->publish(this->get_state_topic_(), state_s);
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -132,17 +132,29 @@ std::string MQTTComponent::get_command_topic_() const {
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
|
||||
return this->publish(topic, payload.data(), payload.size());
|
||||
return this->publish(topic.c_str(), payload.data(), payload.size());
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const std::string &topic, const char *payload, size_t payload_length) {
|
||||
if (topic.empty())
|
||||
return this->publish(topic.c_str(), payload, payload_length);
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const char *topic, const char *payload, size_t payload_length) {
|
||||
if (topic[0] == '\0')
|
||||
return false;
|
||||
return global_mqtt_client->publish(topic, payload, payload_length, this->qos_, this->retain_);
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const char *topic, const char *payload) {
|
||||
return this->publish(topic, payload, strlen(payload));
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
|
||||
if (topic.empty())
|
||||
return this->publish_json(topic.c_str(), f);
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f) {
|
||||
if (topic[0] == '\0')
|
||||
return false;
|
||||
return global_mqtt_client->publish_json(topic, f, this->qos_, this->retain_);
|
||||
}
|
||||
|
||||
@@ -157,6 +157,38 @@ class MQTTComponent : public Component {
|
||||
*/
|
||||
bool publish(const std::string &topic, const char *payload, size_t payload_length);
|
||||
|
||||
/** Send a MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as C string.
|
||||
* @param payload The payload buffer.
|
||||
* @param payload_length The length of the payload.
|
||||
*/
|
||||
bool publish(const char *topic, const char *payload, size_t payload_length);
|
||||
|
||||
/** Send a MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
|
||||
* @param payload The payload buffer.
|
||||
* @param payload_length The length of the payload.
|
||||
*/
|
||||
bool publish(StringRef topic, const char *payload, size_t payload_length) {
|
||||
return this->publish(topic.c_str(), payload, payload_length);
|
||||
}
|
||||
|
||||
/** Send a MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as C string.
|
||||
* @param payload The null-terminated payload.
|
||||
*/
|
||||
bool publish(const char *topic, const char *payload);
|
||||
|
||||
/** Send a MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
|
||||
* @param payload The null-terminated payload.
|
||||
*/
|
||||
bool publish(StringRef topic, const char *payload) { return this->publish(topic.c_str(), payload); }
|
||||
|
||||
/** Construct and send a JSON MQTT message.
|
||||
*
|
||||
* @param topic The topic.
|
||||
@@ -164,6 +196,20 @@ class MQTTComponent : public Component {
|
||||
*/
|
||||
bool publish_json(const std::string &topic, const json::json_build_t &f);
|
||||
|
||||
/** Construct and send a JSON MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as C string.
|
||||
* @param f The Json Message builder.
|
||||
*/
|
||||
bool publish_json(const char *topic, const json::json_build_t &f);
|
||||
|
||||
/** Construct and send a JSON MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
|
||||
* @param f The Json Message builder.
|
||||
*/
|
||||
bool publish_json(StringRef topic, const json::json_build_t &f) { return this->publish_json(topic.c_str(), f); }
|
||||
|
||||
/** Subscribe to a MQTT topic.
|
||||
*
|
||||
* @param topic The topic. Wildcards are currently not supported.
|
||||
|
||||
@@ -115,7 +115,8 @@ bool MQTTCoverComponent::publish_state() {
|
||||
: this->cover_->position == COVER_OPEN ? "open"
|
||||
: traits.get_supports_position() ? "open"
|
||||
: "unknown";
|
||||
if (!this->publish(this->get_state_topic_(), state_s))
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
|
||||
success = false;
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@ bool MQTTDateComponent::send_initial_state() {
|
||||
}
|
||||
}
|
||||
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
|
||||
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish_json(this->get_state_topic_to_(topic_buf), [year, month, day](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[ESPHOME_F("year")] = year;
|
||||
root[ESPHOME_F("month")] = month;
|
||||
|
||||
@@ -66,15 +66,17 @@ bool MQTTDateTimeComponent::send_initial_state() {
|
||||
}
|
||||
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
|
||||
uint8_t second) {
|
||||
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[ESPHOME_F("year")] = year;
|
||||
root[ESPHOME_F("month")] = month;
|
||||
root[ESPHOME_F("day")] = day;
|
||||
root[ESPHOME_F("hour")] = hour;
|
||||
root[ESPHOME_F("minute")] = minute;
|
||||
root[ESPHOME_F("second")] = second;
|
||||
});
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish_json(this->get_state_topic_to_(topic_buf),
|
||||
[year, month, day, hour, minute, second](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[ESPHOME_F("year")] = year;
|
||||
root[ESPHOME_F("month")] = month;
|
||||
root[ESPHOME_F("day")] = day;
|
||||
root[ESPHOME_F("hour")] = hour;
|
||||
root[ESPHOME_F("minute")] = minute;
|
||||
root[ESPHOME_F("second")] = second;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -44,7 +44,8 @@ void MQTTEventComponent::dump_config() {
|
||||
}
|
||||
|
||||
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
|
||||
return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish_json(this->get_state_topic_to_(topic_buf), [event_type](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[MQTT_EVENT_TYPE] = event_type;
|
||||
});
|
||||
|
||||
@@ -158,9 +158,10 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig
|
||||
}
|
||||
}
|
||||
bool MQTTFanComponent::publish_state() {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
const char *state_s = this->state_->state ? "ON" : "OFF";
|
||||
ESP_LOGD(TAG, "'%s' Sending state %s.", this->state_->get_name().c_str(), state_s);
|
||||
this->publish(this->get_state_topic_(), state_s);
|
||||
this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
||||
bool failed = false;
|
||||
if (this->state_->get_traits().supports_direction()) {
|
||||
bool success = this->publish(this->get_direction_state_topic(),
|
||||
|
||||
@@ -34,7 +34,8 @@ void MQTTJSONLightComponent::on_light_remote_values_update() {
|
||||
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
|
||||
|
||||
bool MQTTJSONLightComponent::publish_state_() {
|
||||
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish_json(this->get_state_topic_to_(topic_buf), [this](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
LightJSONSchema::dump_json(*this->state_, root);
|
||||
});
|
||||
|
||||
@@ -47,13 +47,14 @@ void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfi
|
||||
bool MQTTLockComponent::send_initial_state() { return this->publish_state(); }
|
||||
|
||||
bool MQTTLockComponent::publish_state() {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
char buf[LOCK_STATE_STR_SIZE];
|
||||
strncpy_P(buf, (PGM_P) lock_state_to_string(this->lock_->state), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
return this->publish(this->get_state_topic_(), buf);
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), buf);
|
||||
#else
|
||||
return this->publish(this->get_state_topic_(), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -74,9 +74,10 @@ bool MQTTNumberComponent::send_initial_state() {
|
||||
}
|
||||
}
|
||||
bool MQTTNumberComponent::publish_state(float value) {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
char buffer[64];
|
||||
buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
|
||||
return this->publish(this->get_state_topic_(), buffer);
|
||||
size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), buffer, len);
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -50,7 +50,8 @@ bool MQTTSelectComponent::send_initial_state() {
|
||||
}
|
||||
}
|
||||
bool MQTTSelectComponent::publish_state(const std::string &value) {
|
||||
return this->publish(this->get_state_topic_(), value);
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -79,12 +79,13 @@ bool MQTTSensorComponent::send_initial_state() {
|
||||
}
|
||||
}
|
||||
bool MQTTSensorComponent::publish_state(float value) {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (mqtt::global_mqtt_client->is_publish_nan_as_none() && std::isnan(value))
|
||||
return this->publish(this->get_state_topic_(), "None", 4);
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), "None", 4);
|
||||
int8_t accuracy = this->sensor_->get_accuracy_decimals();
|
||||
char buf[VALUE_ACCURACY_MAX_LEN];
|
||||
size_t len = value_accuracy_to_buf(buf, value, accuracy);
|
||||
return this->publish(this->get_state_topic_(), buf, len);
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), buf, len);
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -52,8 +52,9 @@ void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
|
||||
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }
|
||||
|
||||
bool MQTTSwitchComponent::publish_state(bool state) {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
const char *state_s = state ? "ON" : "OFF";
|
||||
return this->publish(this->get_state_topic_(), state_s);
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -53,7 +53,8 @@ bool MQTTTextComponent::send_initial_state() {
|
||||
}
|
||||
}
|
||||
bool MQTTTextComponent::publish_state(const std::string &value) {
|
||||
return this->publish(this->get_state_topic_(), value);
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -31,7 +31,10 @@ void MQTTTextSensor::dump_config() {
|
||||
LOG_MQTT_COMPONENT(true, false);
|
||||
}
|
||||
|
||||
bool MQTTTextSensor::publish_state(const std::string &value) { return this->publish(this->get_state_topic_(), value); }
|
||||
bool MQTTTextSensor::publish_state(const std::string &value) {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
|
||||
}
|
||||
bool MQTTTextSensor::send_initial_state() {
|
||||
if (this->sensor_->has_state()) {
|
||||
return this->publish_state(this->sensor_->state);
|
||||
|
||||
@@ -53,7 +53,8 @@ bool MQTTTimeComponent::send_initial_state() {
|
||||
}
|
||||
}
|
||||
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
|
||||
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish_json(this->get_state_topic_to_(topic_buf), [hour, minute, second](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[ESPHOME_F("hour")] = hour;
|
||||
root[ESPHOME_F("minute")] = minute;
|
||||
|
||||
@@ -28,7 +28,8 @@ void MQTTUpdateComponent::setup() {
|
||||
}
|
||||
|
||||
bool MQTTUpdateComponent::publish_state() {
|
||||
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish_json(this->get_state_topic_to_(topic_buf), [this](JsonObject root) {
|
||||
root[ESPHOME_F("installed_version")] = this->update_->update_info.current_version;
|
||||
root[ESPHOME_F("latest_version")] = this->update_->update_info.latest_version;
|
||||
root[ESPHOME_F("title")] = this->update_->update_info.title;
|
||||
|
||||
@@ -84,7 +84,8 @@ bool MQTTValveComponent::publish_state() {
|
||||
: this->valve_->position == VALVE_OPEN ? "open"
|
||||
: traits.get_supports_position() ? "open"
|
||||
: "unknown";
|
||||
if (!this->publish(this->get_state_topic_(), state_s))
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
|
||||
success = false;
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -133,17 +133,14 @@ void RD03DComponent::process_frame_() {
|
||||
uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE);
|
||||
|
||||
// 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_high = this->buffer_[offset + 1];
|
||||
uint8_t y_low = this->buffer_[offset + 2];
|
||||
uint8_t y_high = this->buffer_[offset + 3];
|
||||
uint8_t res_low = this->buffer_[offset + 4];
|
||||
uint8_t res_high = this->buffer_[offset + 5];
|
||||
uint8_t speed_low = this->buffer_[offset + 6];
|
||||
uint8_t speed_high = this->buffer_[offset + 7];
|
||||
uint8_t speed_low = this->buffer_[offset + 4];
|
||||
uint8_t speed_high = this->buffer_[offset + 5];
|
||||
uint8_t res_low = this->buffer_[offset + 6];
|
||||
uint8_t res_high = this->buffer_[offset + 7];
|
||||
|
||||
// Decode values per RD-03D format
|
||||
int16_t x = decode_value(x_low, x_high);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "rpi_dpi_rgb.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -135,11 +134,8 @@ void RpiDpiRgb::dump_config() {
|
||||
LOG_PIN(" Enable Pin: ", this->enable_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
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++) {
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
void RpiDpiRgb::reset_display_() const {
|
||||
|
||||
@@ -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 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) {
|
||||
switch (mode) {
|
||||
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() {
|
||||
// the sensor needs 1000 ms to enter the idle state
|
||||
this->set_timeout(1000, [this]() {
|
||||
@@ -97,18 +75,18 @@ void SEN5XComponent::setup() {
|
||||
stop_measurement_delay = 200;
|
||||
}
|
||||
this->set_timeout(stop_measurement_delay, [this]() {
|
||||
// note: serial number register is actually 32-bytes long but we grab only the first 16-bytes,
|
||||
// this appears to be all that Sensirion uses for serial numbers, this could change
|
||||
uint16_t raw_serial_number[8];
|
||||
if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 8, 20)) {
|
||||
uint16_t raw_serial_number[3];
|
||||
if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) {
|
||||
ESP_LOGE(TAG, "Failed to read serial number");
|
||||
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
const char *serial_number = sensirion_convert_to_string_in_place(raw_serial_number, 8);
|
||||
snprintf(this->serial_number_, sizeof(this->serial_number_), "%s", serial_number);
|
||||
ESP_LOGV(TAG, "Serial number %s", this->serial_number_);
|
||||
this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF);
|
||||
this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF);
|
||||
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];
|
||||
if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
|
||||
@@ -117,35 +95,50 @@ void SEN5XComponent::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
const char *product_name = sensirion_convert_to_string_in_place(raw_product_name, 16);
|
||||
if (strncmp(product_name, "SEN50", 5) == 0) {
|
||||
this->type_ = Sen5xType::SEN50;
|
||||
} else if (strncmp(product_name, "SEN54", 5) == 0) {
|
||||
this->type_ = Sen5xType::SEN54;
|
||||
} else if (strncmp(product_name, "SEN55", 5) == 0) {
|
||||
this->type_ = Sen5xType::SEN55;
|
||||
} else {
|
||||
this->type_ = Sen5xType::UNKNOWN;
|
||||
ESP_LOGE(TAG, "Unknown product name: %.32s", product_name);
|
||||
this->error_code_ = PRODUCT_NAME_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// 2 ASCII bytes are encoded in an int
|
||||
const uint16_t *current_int = raw_product_name;
|
||||
char current_char;
|
||||
uint8_t max = 16;
|
||||
do {
|
||||
// first char
|
||||
current_char = *current_int >> 8;
|
||||
if (current_char) {
|
||||
this->product_name_.push_back(current_char);
|
||||
// second char
|
||||
current_char = *current_int & 0xFF;
|
||||
if (current_char) {
|
||||
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_)));
|
||||
if (this->humidity_sensor_ && this->type_ == Sen5xType::SEN50) {
|
||||
Sen5xType sen5x_type = UNKNOWN;
|
||||
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");
|
||||
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");
|
||||
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");
|
||||
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");
|
||||
this->nox_sensor_ = nullptr; // mark as not used
|
||||
}
|
||||
@@ -160,25 +153,43 @@ void SEN5XComponent::setup() {
|
||||
ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_);
|
||||
|
||||
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 hash = fnv1a_hash(this->serial_number_);
|
||||
this->pref_ = global_preferences->make_preference<uint16_t[4]>(hash, true);
|
||||
this->voc_baseline_time_ = App.get_loop_component_start_time();
|
||||
if (this->pref_.load(&this->voc_baseline_state_)) {
|
||||
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) {
|
||||
ESP_LOGE(TAG, "VOC Baseline State write to sensor failed");
|
||||
} else {
|
||||
ESP_LOGV(TAG, "VOC Baseline State loaded");
|
||||
delay(20);
|
||||
uint32_t combined_serial =
|
||||
encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]);
|
||||
// Hash with config hash, version, and serial number
|
||||
// This ensures the baseline storage is cleared after OTA
|
||||
// Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
|
||||
uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial);
|
||||
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
|
||||
|
||||
if (this->pref_.load(&this->voc_baselines_storage_)) {
|
||||
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;
|
||||
if (this->auto_cleaning_interval_.has_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 {
|
||||
result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
|
||||
result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
|
||||
}
|
||||
if (result) {
|
||||
delay(20);
|
||||
@@ -265,10 +276,11 @@ void SEN5XComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Type: %s\n"
|
||||
" Product name: %s\n"
|
||||
" Firmware version: %d\n"
|
||||
" Serial number: %s",
|
||||
LOG_STR_ARG(type_to_string(this->type_)), this->firmware_version_, this->serial_number_);
|
||||
" Serial number %02d.%02d.%02d",
|
||||
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()) {
|
||||
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",
|
||||
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_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
|
||||
@@ -300,6 +304,36 @@ void SEN5XComponent::update() {
|
||||
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)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_);
|
||||
@@ -368,29 +402,7 @@ void SEN5XComponent::update() {
|
||||
if (this->nox_sensor_ != nullptr) {
|
||||
this->nox_sensor_->publish_state(nox);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this->status_clear_warning();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,10 @@ enum RhtAccelerationMode : uint16_t {
|
||||
HIGH_ACCELERATION = 2,
|
||||
};
|
||||
|
||||
enum class Sen5xType : uint8_t { SEN50, SEN54, SEN55, UNKNOWN };
|
||||
struct Sen5xBaselines {
|
||||
int32_t state0;
|
||||
int32_t state1;
|
||||
} PACKED; // NOLINT
|
||||
|
||||
struct GasTuning {
|
||||
uint16_t index_offset;
|
||||
@@ -41,9 +44,11 @@ struct TemperatureCompensation {
|
||||
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
|
||||
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 {
|
||||
public:
|
||||
@@ -51,20 +56,20 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { this->pm_1_0_sensor_ = pm_1_0; }
|
||||
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; }
|
||||
enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN };
|
||||
|
||||
void set_voc_sensor(sensor::Sensor *voc_sensor) { this->voc_sensor_ = voc_sensor; }
|
||||
void set_nox_sensor(sensor::Sensor *nox_sensor) { this->nox_sensor_ = nox_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||
void set_store_baseline(bool store_baseline) { this->store_baseline_ = store_baseline; }
|
||||
void set_acceleration_mode(RhtAccelerationMode mode) { this->acceleration_mode_ = mode; }
|
||||
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) {
|
||||
this->auto_cleaning_interval_ = auto_cleaning_interval;
|
||||
}
|
||||
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; }
|
||||
void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; }
|
||||
void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; }
|
||||
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; }
|
||||
|
||||
void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
|
||||
void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
|
||||
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,
|
||||
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
|
||||
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.std_initial = std_initial;
|
||||
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,
|
||||
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.std_initial = 50;
|
||||
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) {
|
||||
TemperatureCompensation temp_comp;
|
||||
temp_comp.offset = offset * 200;
|
||||
temp_comp.normalized_offset_slope = normalized_offset_slope * 10000;
|
||||
temp_comp.time_constant = time_constant;
|
||||
this->temperature_compensation_ = temp_comp;
|
||||
temperature_compensation_ = temp_comp;
|
||||
}
|
||||
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_temperature_compensation_(const TemperatureCompensation &compensation);
|
||||
|
||||
char serial_number_[17] = "UNKNOWN";
|
||||
uint16_t voc_baseline_state_[4]{0};
|
||||
uint32_t voc_baseline_time_;
|
||||
uint32_t seconds_since_last_store_;
|
||||
uint16_t firmware_version_;
|
||||
Sen5xType type_{Sen5xType::UNKNOWN};
|
||||
ERRORCODE error_code_;
|
||||
uint8_t serial_number_[4];
|
||||
bool initialized_{false};
|
||||
bool store_baseline_;
|
||||
|
||||
@@ -128,6 +131,8 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
|
||||
optional<GasTuning> nox_tuning_params_;
|
||||
optional<TemperatureCompensation> temperature_compensation_;
|
||||
ESPPreferenceObject pref_;
|
||||
std::string product_name_;
|
||||
Sen5xBaselines voc_baselines_storage_;
|
||||
};
|
||||
|
||||
} // namespace sen5x
|
||||
|
||||
@@ -210,7 +210,6 @@ SENSOR_MAP = {
|
||||
SETTING_MAP = {
|
||||
CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval",
|
||||
CONF_ACCELERATION_MODE: "set_acceleration_mode",
|
||||
CONF_STORE_BASELINE: "set_store_baseline",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
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;
|
||||
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
|
||||
uint8_t raw_idx = 0;
|
||||
if (command_len == 1) {
|
||||
temp[raw_idx++] = command & 0xFF;
|
||||
} else {
|
||||
// command is 2 bytes
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
temp[raw_idx++] = command >> 8;
|
||||
temp[raw_idx++] = command & 0xFF;
|
||||
#else
|
||||
temp[raw_idx++] = command & 0xFF;
|
||||
temp[raw_idx++] = command >> 8;
|
||||
#endif
|
||||
}
|
||||
// add parameters followed by crc
|
||||
// skipped if len == 0
|
||||
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] & 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
|
||||
uint8_t crc = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true);
|
||||
temp[raw_idx++] = crc;
|
||||
|
||||
@@ -445,18 +445,22 @@ optional<float> CalibratePolynomialFilter::new_value(float value) {
|
||||
ClampFilter::ClampFilter(float min, float max, bool ignore_out_of_range)
|
||||
: min_(min), max_(max), ignore_out_of_range_(ignore_out_of_range) {}
|
||||
optional<float> ClampFilter::new_value(float value) {
|
||||
if (std::isfinite(this->min_) && !(value >= this->min_)) {
|
||||
if (this->ignore_out_of_range_) {
|
||||
return {};
|
||||
if (std::isfinite(value)) {
|
||||
if (std::isfinite(this->min_) && value < this->min_) {
|
||||
if (this->ignore_out_of_range_) {
|
||||
return {};
|
||||
} else {
|
||||
return this->min_;
|
||||
}
|
||||
}
|
||||
return this->min_;
|
||||
}
|
||||
|
||||
if (std::isfinite(this->max_) && !(value <= this->max_)) {
|
||||
if (this->ignore_out_of_range_) {
|
||||
return {};
|
||||
if (std::isfinite(this->max_) && value > this->max_) {
|
||||
if (this->ignore_out_of_range_) {
|
||||
return {};
|
||||
} else {
|
||||
return this->max_;
|
||||
}
|
||||
}
|
||||
return this->max_;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "slow_pwm_output.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace slow_pwm {
|
||||
@@ -21,9 +20,7 @@ void SlowPWMOutput::set_output_state_(bool new_state) {
|
||||
}
|
||||
if (new_state != current_state_) {
|
||||
if (this->pin_) {
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
this->pin_->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGV(TAG, "Switching output pin %s to %s", pin_summary, ONOFF(new_state));
|
||||
ESP_LOGV(TAG, "Switching output pin %s to %s", this->pin_->dump_summary().c_str(), ONOFF(new_state));
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Switching to %s", ONOFF(new_state));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "st7701s.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -184,11 +183,8 @@ void ST7701S::dump_config() {
|
||||
LOG_PIN(" DE Pin: ", this->de_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
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++) {
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
|
||||
}
|
||||
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());
|
||||
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000));
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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, ®_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, ®_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
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -20,7 +20,7 @@ from .. import template_ns
|
||||
CONF_CURRENT_TEMPERATURE = "current_temperature"
|
||||
|
||||
TemplateWaterHeater = template_ns.class_(
|
||||
"TemplateWaterHeater", cg.Component, water_heater.WaterHeater
|
||||
"TemplateWaterHeater", water_heater.WaterHeater
|
||||
)
|
||||
|
||||
TemplateWaterHeaterPublishAction = template_ns.class_(
|
||||
@@ -36,29 +36,24 @@ RESTORE_MODES = {
|
||||
"RESTORE_AND_CALL": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE_AND_CALL,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
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_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_SUPPORTED_MODES): cv.ensure_list(
|
||||
water_heater.validate_water_heater_mode
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = 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_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_SUPPORTED_MODES): cv.ensure_list(
|
||||
water_heater.validate_water_heater_mode
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await water_heater.register_water_heater(var, config)
|
||||
|
||||
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
|
||||
|
||||
@@ -10,7 +10,7 @@ TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {}
|
||||
void TemplateWaterHeater::setup() {
|
||||
if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE ||
|
||||
this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE_AND_CALL) {
|
||||
auto restore = this->restore_state_();
|
||||
auto restore = this->restore_state();
|
||||
|
||||
if (restore.has_value()) {
|
||||
restore->perform();
|
||||
|
||||
@@ -13,7 +13,7 @@ enum TemplateWaterHeaterRestoreMode {
|
||||
WATER_HEATER_RESTORE_AND_CALL,
|
||||
};
|
||||
|
||||
class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
||||
class TemplateWaterHeater : public water_heater::WaterHeater {
|
||||
public:
|
||||
TemplateWaterHeater();
|
||||
|
||||
|
||||
@@ -1060,11 +1060,11 @@ bool ThermostatClimate::cooling_required_() {
|
||||
auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature;
|
||||
|
||||
if (this->supports_cool_) {
|
||||
if (this->current_temperature >= temperature + this->cooling_deadband_) {
|
||||
// if the current temperature reaches or exceeds the target + deadband, cooling is required
|
||||
if (this->current_temperature > temperature + this->cooling_deadband_) {
|
||||
// if the current temperature exceeds the target + deadband, cooling is required
|
||||
return true;
|
||||
} 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
|
||||
} else if (this->current_temperature < temperature - this->cooling_overrun_) {
|
||||
// if the current temperature is less than the target - overrun, cooling should stop
|
||||
return false;
|
||||
} else {
|
||||
// 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_cooling_) {
|
||||
if (this->current_temperature >= temperature + this->cooling_deadband_) {
|
||||
// if the current temperature reaches or exceeds the target + deadband, fanning is required
|
||||
if (this->current_temperature > temperature + this->cooling_deadband_) {
|
||||
// if the current temperature exceeds the target + deadband, fanning is required
|
||||
return true;
|
||||
} 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
|
||||
} else if (this->current_temperature < temperature - this->cooling_overrun_) {
|
||||
// if the current temperature is less than the target - overrun, fanning should stop
|
||||
return false;
|
||||
} else {
|
||||
// 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;
|
||||
|
||||
if (this->supports_heat_) {
|
||||
if (this->current_temperature <= temperature - this->heating_deadband_) {
|
||||
// if the current temperature is below or equal to the target - deadband, heating is required
|
||||
if (this->current_temperature < temperature - this->heating_deadband_) {
|
||||
// if the current temperature is below the target - deadband, heating is required
|
||||
return true;
|
||||
} else if (this->current_temperature >= temperature + this->heating_overrun_) {
|
||||
// if the current temperature is above or equal to the target + overrun, heating should stop
|
||||
|
||||
} else if (this->current_temperature > temperature + this->heating_overrun_) {
|
||||
// if the current temperature is above the target + overrun, heating should stop
|
||||
return false;
|
||||
} else {
|
||||
// if we get here, the current temperature is between target - deadband and target + overrun,
|
||||
|
||||
@@ -40,9 +40,6 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
// Unsigned subtraction handles wraparound correctly, then cast to signed
|
||||
int32_t diff = static_cast<int32_t>(epoch - static_cast<uint32_t>(current_time));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ CODEOWNERS = ["@dhoeben"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
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")
|
||||
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"))
|
||||
|
||||
@@ -91,6 +91,8 @@ async def register_water_heater(var: cg.Pvariable, config: ConfigType) -> cg.Pva
|
||||
|
||||
cg.add_define("USE_WATER_HEATER")
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(cg.App.register_water_heater(var))
|
||||
|
||||
CORE.register_platform_component("water_heater", var)
|
||||
|
||||
@@ -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() {
|
||||
auto traits = this->get_traits();
|
||||
ESP_LOGD(TAG,
|
||||
@@ -184,8 +188,7 @@ void WaterHeater::publish_state() {
|
||||
this->pref_.save(&saved);
|
||||
}
|
||||
|
||||
optional<WaterHeaterCall> WaterHeater::restore_state_() {
|
||||
this->pref_ = global_preferences->make_preference<SavedWaterHeaterState>(this->get_preference_hash());
|
||||
optional<WaterHeaterCall> WaterHeater::restore_state() {
|
||||
SavedWaterHeaterState recovered{};
|
||||
if (!this->pref_.load(&recovered))
|
||||
return {};
|
||||
|
||||
@@ -177,7 +177,7 @@ class WaterHeaterTraits {
|
||||
WaterHeaterModeMask supported_modes_;
|
||||
};
|
||||
|
||||
class WaterHeater : public EntityBase {
|
||||
class WaterHeater : public EntityBase, public Component {
|
||||
public:
|
||||
WaterHeaterMode get_mode() const { return this->mode_; }
|
||||
float get_current_temperature() const { return this->current_temperature_; }
|
||||
@@ -204,15 +204,16 @@ class WaterHeater : public EntityBase {
|
||||
#endif
|
||||
virtual void control(const WaterHeaterCall &call) = 0;
|
||||
|
||||
void setup() override;
|
||||
|
||||
optional<WaterHeaterCall> restore_state();
|
||||
|
||||
protected:
|
||||
virtual WaterHeaterTraits traits() = 0;
|
||||
|
||||
/// Log the traits of this water heater for dump_config().
|
||||
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().
|
||||
void set_mode_(WaterHeaterMode mode) { this->mode_ = mode; }
|
||||
/// Set the target temperature of the water heater. Should only be called from control().
|
||||
|
||||
@@ -31,7 +31,6 @@ from esphome.const import (
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
@@ -214,7 +213,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_RTL87XX,
|
||||
]
|
||||
),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user