mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 18:22:48 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0b0cd6e13 | ||
|
|
18e93a96fc | ||
|
|
01f1139476 | ||
|
|
053cb337aa | ||
|
|
3ac5918ec8 | ||
|
|
bf5d095cf6 | ||
|
|
eae1b99232 | ||
|
|
15936f9a55 | ||
|
|
aac085c5c7 | ||
|
|
fe66ac546a | ||
|
|
a8e5fd0d2f | ||
|
|
93afb0d092 |
@@ -42,8 +42,8 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo,
|
||||
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
||||
|
||||
message +=
|
||||
`Hey @${prAuthor}, thanks for the contribution. This PR is on the ` +
|
||||
`large side `;
|
||||
`Hey @${prAuthor}, thanks for the contribution! Just a heads up, ` +
|
||||
`this PR is on the large side `;
|
||||
|
||||
if (tooManyLabels && tooManyChanges) {
|
||||
message +=
|
||||
@@ -57,31 +57,16 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo,
|
||||
}
|
||||
|
||||
message += `, which makes it harder for maintainers to review.\n\n`;
|
||||
let condition;
|
||||
if (tooManyLabels && tooManyChanges) {
|
||||
condition =
|
||||
`is at most ${TOO_BIG_THRESHOLD} lines (excluding tests) and touches ` +
|
||||
`at most ${MAX_LABELS} components/areas`;
|
||||
} else if (tooManyLabels) {
|
||||
condition = `touches at most ${MAX_LABELS} components/areas`;
|
||||
} else {
|
||||
condition = `is at most ${TOO_BIG_THRESHOLD} lines (excluding tests)`;
|
||||
}
|
||||
message +=
|
||||
`**This PR will not be reviewed until it ${condition}.** We know ` +
|
||||
`that is not what you want to hear after this much work, and we want ` +
|
||||
`to be upfront so you do not wait for a review that is not coming: ` +
|
||||
`please split the change. The label is removed automatically once ` +
|
||||
`the PR ${condition}.\n\n`;
|
||||
`Smaller, focused PRs tend to be reviewed much faster since they ` +
|
||||
`fit into the short gaps between other maintainer work; large ones ` +
|
||||
`often have to wait for a rare long uninterrupted block of time. ` +
|
||||
`If you can break this up into smaller pieces that can be reviewed ` +
|
||||
`independently, it will almost certainly land faster overall.\n\n`;
|
||||
message +=
|
||||
`There is no exception you can request. Rarely, a maintainer agrees in ` +
|
||||
`\`#devs\` on [Discord](https://esphome.io/chat) to review a change that ` +
|
||||
`genuinely cannot be split as one piece and adds the \`mega-pr\` label, ` +
|
||||
`but that is rare enough that you should plan on splitting rather than ` +
|
||||
`wait for it. Smaller, focused PRs are picked up much faster because ` +
|
||||
`they fit into the short gaps between other maintainer work. For a new ` +
|
||||
`component, the first PR brings the hub and a single platform (for ` +
|
||||
`example \`sensor\`); each further platform is its own PR on top of it.\n\n`;
|
||||
`Before putting more time in, it's also worth popping into ` +
|
||||
`\`#devs\` on [Discord](https://esphome.io/chat) so we can help ` +
|
||||
`you scope things and flag anything already in flight.\n\n`;
|
||||
message +=
|
||||
`For more details (including how to split the work up), see: ` +
|
||||
`https://developers.esphome.io/contributing/submitting-your-work/` +
|
||||
@@ -229,7 +214,6 @@ async function handleMaintainerAccessComment(github, context, maintainerAccess)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateReviewMessages,
|
||||
handleReviews,
|
||||
handleMaintainerAccessComment
|
||||
};
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { generateReviewMessages } = require('../reviews');
|
||||
|
||||
const MAX_LABELS = 15;
|
||||
const TOO_BIG_THRESHOLD = 1000;
|
||||
|
||||
function tooBigMessage(labelCount, additions) {
|
||||
const files = [{ filename: 'esphome/components/a/a.cpp', additions, deletions: 0 }];
|
||||
const messages = generateReviewMessages(['too-big'], labelCount, null, files, additions, 0, 'someone', MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||
assert.equal(messages.length, 1);
|
||||
return messages[0];
|
||||
}
|
||||
|
||||
describe('too-big review message', () => {
|
||||
it('names the line cap when the PR is too long', () => {
|
||||
const message = tooBigMessage(3, 1500);
|
||||
assert.match(message, /will not be reviewed until it is at most 1000 lines \(excluding tests\)\./);
|
||||
assert.match(message, /removed automatically once the PR is at most 1000 lines/);
|
||||
assert.doesNotMatch(message, /components\/areas\./);
|
||||
});
|
||||
|
||||
it('names the area cap when the PR touches too many areas but is short', () => {
|
||||
const message = tooBigMessage(20, 200);
|
||||
assert.match(message, /will not be reviewed until it touches at most 15 components\/areas\./);
|
||||
assert.doesNotMatch(message, /until it is at most 1000 lines/);
|
||||
});
|
||||
|
||||
it('names both caps when both are exceeded', () => {
|
||||
const message = tooBigMessage(20, 1500);
|
||||
assert.match(message, /is at most 1000 lines \(excluding tests\) and touches at most 15 components\/areas\./);
|
||||
});
|
||||
|
||||
it('tells new-component authors to bring one platform first', () => {
|
||||
assert.match(tooBigMessage(3, 1500), /hub and a single platform/);
|
||||
assert.match(tooBigMessage(3, 1500), /mega-pr/);
|
||||
});
|
||||
});
|
||||
@@ -1335,91 +1335,6 @@ jobs:
|
||||
# config first, so a separate config pass is redundant.
|
||||
python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests
|
||||
|
||||
test-esp8266-native-windows:
|
||||
name: Compile esp8266 with the native toolchain on Windows (ccache ${{ matrix.ccache }})
|
||||
runs-on: windows-latest
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.esp8266-native == 'true'
|
||||
# A hung compiler must fail the job, not hold the runner for six hours.
|
||||
timeout-minutes: 40
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Both ways the build runs on Windows: ESPHome Desktop bundles
|
||||
# ccache and puts it on PATH, a pip install has none.
|
||||
ccache: ["1", "0"]
|
||||
env:
|
||||
ESPHOME_CCACHE_ENABLE: ${{ matrix.ccache }}
|
||||
# The test script prints characters the cp1252 console cannot encode
|
||||
PYTHONUTF8: "1"
|
||||
# The exact static build ESPHome Desktop ships
|
||||
CCACHE_URL: https://github.com/ccache/ccache/releases/download/v4.14/ccache-4.14-windows-x86_64.zip
|
||||
CCACHE_SHA256: 2568347a697e103ca1b073981c704ad76fb2507d066c38dba038dd73399d968f
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Install ccache
|
||||
if: matrix.ccache == '1'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$dir = Join-Path $env:RUNNER_TEMP "ccache"
|
||||
New-Item -ItemType Directory -Force $dir | Out-Null
|
||||
$zip = Join-Path $dir "ccache.zip"
|
||||
Invoke-WebRequest -Uri $env:CCACHE_URL -OutFile $zip
|
||||
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
|
||||
if ($hash -ne $env:CCACHE_SHA256) {
|
||||
Write-Host "::error::ccache archive sha256 $hash does not match $env:CCACHE_SHA256"
|
||||
exit 1
|
||||
}
|
||||
Expand-Archive -Path $zip -DestinationPath $dir
|
||||
$bin = Get-ChildItem -Path $dir -Recurse -Filter ccache.exe | Select-Object -First 1
|
||||
Add-Content -Path $env:GITHUB_PATH -Value $bin.DirectoryName
|
||||
& $bin.FullName --version
|
||||
|
||||
- name: Compile
|
||||
shell: pwsh
|
||||
# One component test: web_server brings wifi, api and the async web
|
||||
# server libraries, the shape of a typical ESP8266 device
|
||||
run: |
|
||||
. .\venv\Scripts\Activate.ps1
|
||||
$arguments = "script/test_build_components.py", "-e", "compile", "-t", "esp8266-ard", "-c", "web_server", "--base-only", "--toolchain", "arduino", "--fail-on-no-tests"
|
||||
$p = Start-Process -FilePath python -ArgumentList $arguments -NoNewWindow -PassThru
|
||||
# Reading the handle keeps ExitCode available after the process exits
|
||||
$null = $p.Handle
|
||||
if (-not $p.WaitForExit(25 * 60 * 1000)) {
|
||||
Write-Host "::error::The compile did not finish in 25 minutes; processes still running:"
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object { $_.CommandLine -match 'xtensa|ccache|ninja|esphome' } |
|
||||
Select-Object ProcessId, ParentProcessId, CreationDate, CommandLine |
|
||||
Format-List | Out-String -Width 4096 | Write-Host
|
||||
Get-Process |
|
||||
Where-Object { $_.Name -match 'cc1|ccache|ninja|xtensa|python' } |
|
||||
Select-Object Id, Name, CPU, WorkingSet64 |
|
||||
Format-Table | Out-String | Write-Host
|
||||
taskkill /PID $p.Id /T /F
|
||||
exit 1
|
||||
}
|
||||
exit $p.ExitCode
|
||||
|
||||
- name: Show ccache statistics
|
||||
# Also after a failed or killed compile: the counters say whether
|
||||
# ccache was ever reached
|
||||
if: always() && matrix.ccache == '1'
|
||||
shell: pwsh
|
||||
# The build keeps its store under the esphome tools cache
|
||||
run: |
|
||||
$env:CCACHE_DIR = Join-Path $env:LOCALAPPDATA "esphome\Cache\arduino8266\ccache"
|
||||
ccache -s
|
||||
|
||||
device-builder:
|
||||
name: Test downstream esphome/device-builder
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -1790,7 +1705,6 @@ jobs:
|
||||
- test-build-components-split
|
||||
- test-esp32-platformio
|
||||
- test-esp8266-native
|
||||
- test-esp8266-native-windows
|
||||
- device-builder
|
||||
- memory-impact-target-branch
|
||||
- memory-impact-pr-branch
|
||||
|
||||
@@ -16,18 +16,16 @@ jobs:
|
||||
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
|
||||
# GitHub App token so the labels, comments and closures come from
|
||||
# esphome[bot] instead of github-actions[bot].
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@4fbce13026c393d6eb6b247e394dcd9ef1af76c2 # main
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # main
|
||||
secrets:
|
||||
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
# Live only on dev: a workflow_dispatch from any other branch is a dry run
|
||||
dry-run: ${{ github.ref != 'refs/heads/dev' }}
|
||||
days-before-stale: 90
|
||||
days-before-stale-conflicting: 30
|
||||
days-before-close: 7
|
||||
stale-label: stale
|
||||
exempt-label: not-stale
|
||||
conflict-label: needs-rebase
|
||||
ignored-users: esphbot,codecov-commenter
|
||||
stale-pr-message: >
|
||||
There hasn't been any activity on this pull request recently. This
|
||||
@@ -38,18 +36,6 @@ jobs:
|
||||
to keep it open. Also, please merge the latest dev branch into your
|
||||
branch to ensure that it's up to date with the latest changes.
|
||||
|
||||
Thank you for your contribution!
|
||||
conflict-pr-message: >
|
||||
This pull request has merge conflicts. Please merge the latest dev
|
||||
branch into your branch to resolve them.
|
||||
stale-conflicting-pr-message: >
|
||||
The merge conflicts on this pull request have not been resolved for
|
||||
30 days, so it has been marked as stale and will be closed if they
|
||||
are not resolved within 7 days.
|
||||
|
||||
Please merge the latest dev branch into your branch. Once the pull
|
||||
request is mergeable again the stale label is removed.
|
||||
|
||||
Thank you for your contribution!
|
||||
stale-issue-message: >
|
||||
There hasn't been any activity on this issue recently. Due to the
|
||||
|
||||
@@ -67,7 +67,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
- Function-local constants: `lower_snake_case`
|
||||
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
|
||||
- Favor descriptive names over abbreviations
|
||||
- Device components are named brand plus model (`siemens_ira211`), without a protocol-family prefix
|
||||
- Enumerator names: prefix every value of an `enum class` with the enum name converted to
|
||||
`UPPER_SNAKE_CASE` (e.g. `UARTFlushResult::UART_FLUSH_RESULT_SUCCESS`). Never use bare
|
||||
names like `SUCCESS`, `FAILURE`, `OK`, or `FAIL`: platform SDK headers define macros with
|
||||
@@ -178,8 +177,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use
|
||||
`set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms:
|
||||
https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb
|
||||
* **Repeating work:** use `set_interval`, never a `set_timeout` that re-arms itself; every re-arm
|
||||
rebuilds the callback and re-queues a scheduler item, and the period drifts.
|
||||
* **Scheduler ids:** name a timer only when it must be cancelled or replaced, and use a
|
||||
`static constexpr uint32_t` id, never a string. Ids are per component instance and cannot clash with other
|
||||
components, so number them from 0 and keep all of a component's ids together in one place.
|
||||
@@ -194,33 +191,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
// Bad - this is exactly what the base class already does
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
```
|
||||
* **Constant data lives in RAM on ESP8266** unless marked `PROGMEM`: `const` tables, class-scope
|
||||
constant arrays, and in-class `std::array` initializers (a `.rodata` template plus a copy). Mark the
|
||||
table `PROGMEM` and read it with `progmem_memcpy()` or `progmem_read_byte()` (`esphome/core/hal.h`);
|
||||
for a table of structs copy the whole entry out and return the copy, never a pointer into flash.
|
||||
On other platforms they are a plain `memcpy` and dereference, so no `#ifdef` is needed. Declare log tags with
|
||||
`ESPHOME_LOG_TAG(TAG, "name")`, pass status text as `LOG_STR`, e.g.
|
||||
`this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL))`, and give an action whose payload is a byte array
|
||||
a `TEMPLATABLE_BYTES` field so a static code stays in flash.
|
||||
* **Fixed sets of names** (status words, model names): a `PROGMEM_STRING_TABLE` (`esphome/core/progmem.h`),
|
||||
logged with `get_log_str` and `LOG_STR_ARG`, copied out with `ESPHOME_strncpy_P` before `publish_state`.
|
||||
A `dump_config` helper that picks a name returns `const LogString *` from `LOG_STR(...)`.
|
||||
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `reverse_bits`, `encode_uint16`);
|
||||
pick the helper's polynomial and bit-order arguments instead of writing a CRC loop.
|
||||
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
|
||||
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
|
||||
end of `setup()` when the entities need a state at boot.
|
||||
* **A multi-step command sequence** leaves the step pending when the answer is malformed, so the existing
|
||||
resend and give-up logic handles it; never advance on an answer that was only logged.
|
||||
* **Entities that each watch a register field:** one class taking register, shift, mask and match from a Python
|
||||
tuple table, in typed `StaticVector`s sized by `cg.slot_counter`; not a template per bit, not a virtual
|
||||
listener base (a second vptr per entity).
|
||||
* **A global feature define** says the code is compiled in, not that this instance uses it: keep the
|
||||
per-instance check (`if (this->aux_device_ == ...)`) inside the `#ifdef`.
|
||||
* **Failures after `setup()`:** `mark_failed()` is for `setup()` only. A runtime read or write failure
|
||||
sets `status_set_warning()` and the next success clears it; writes skip work when `is_failed()`.
|
||||
* **Fixed platform facts compile to nothing:** gate on `#ifdef USE_ESP8266` and friends, not on a
|
||||
runtime check of something the build already knows.
|
||||
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
|
||||
can be stored in flash rather than RAM.
|
||||
```cpp
|
||||
@@ -564,7 +534,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
|
||||
|
||||
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
|
||||
An intentionally empty body (an unused `register_dumper` builder, for example) gets a one-line docstring, not `pass`.
|
||||
|
||||
* **Configuration Validation:**
|
||||
* **Reuse existing validators:** Before writing a custom validator, check for an existing one in `config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components. For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`.
|
||||
@@ -600,15 +569,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
* **Testing:**
|
||||
* **Python:** Run unit tests with `pytest`.
|
||||
* **C++:** Use `clang-tidy` for static analysis.
|
||||
* **C++ unit tests:** gtests in `tests/components/<component>/*.cpp`, run with
|
||||
`script/cpp_unit_test.py <component>`; see `tests/components/README.md`. Set the loop clock in a test
|
||||
with `LoopBlockingGuard{nullptr, nullptr, now_ms}`; the runner resets it before every test.
|
||||
* **Integration tests:** `tests/integration/` runs real firmware on the host. Serial components prove
|
||||
timeouts and recovery with the `uart_mock` fixture (`tests/integration/test_uart_mock_*.py`). Its
|
||||
`responses` match on content every time they see it; to answer a repeated request differently, use an
|
||||
`on_tx` lambda with a static counter and `inject_to_rx_buffer` (see `uart_mock_ld2412_requery_after_giveup.yaml`).
|
||||
* **Keep build-test variants few:** one `test.<platform>.yaml` per platform the code actually differs
|
||||
on. A variant per board for platform-identical code only adds CI time.
|
||||
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
|
||||
```
|
||||
tests/
|
||||
@@ -655,7 +615,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
name: My Sensor
|
||||
```
|
||||
Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time.
|
||||
`!extend` and `!remove` in a `test.*.yaml` also make the component ungroupable; add a standalone entity with its own id instead.
|
||||
|
||||
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
|
||||
```bash
|
||||
|
||||
+3
-40
@@ -39,12 +39,9 @@ esphome/components/am43/sensor/* @buxtronix
|
||||
esphome/components/analog_threshold/* @ianchi
|
||||
esphome/components/animation/* @syndlex
|
||||
esphome/components/anova/* @buxtronix
|
||||
esphome/components/apc1/* @ademuri
|
||||
esphome/components/apds9306/* @aodrenah
|
||||
esphome/components/apds9930/* @iyesin
|
||||
esphome/components/api/* @esphome/core
|
||||
esphome/components/aqi/* @freekode @jasstrong @ximex
|
||||
esphome/components/aranet/* @patrickod
|
||||
esphome/components/as5600/* @ammmze
|
||||
esphome/components/as5600/sensor/* @ammmze
|
||||
esphome/components/as7341/* @mrgnr
|
||||
@@ -68,7 +65,6 @@ esphome/components/bedjet/climate/* @jhansche
|
||||
esphome/components/bedjet/fan/* @jhansche
|
||||
esphome/components/bedjet/sensor/* @javawizard @jhansche
|
||||
esphome/components/beken_spi_led_strip/* @Mat931
|
||||
esphome/components/bh1745/* @latonita
|
||||
esphome/components/bh1750/* @OttoWinter
|
||||
esphome/components/bh1900nux/* @B48D81EFCC
|
||||
esphome/components/binary_sensor/* @esphome/core
|
||||
@@ -104,9 +100,6 @@ esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
|
||||
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
|
||||
esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bq25186/* @Rapsssito
|
||||
esphome/components/bq27220/* @rggammon
|
||||
esphome/components/bq76972/* @limpkin
|
||||
esphome/components/bridge/* @kbx81
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
esphome/components/button/* @esphome/core
|
||||
@@ -163,10 +156,8 @@ esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/ds1603l/* @JakeLC15
|
||||
esphome/components/ds2484/* @mrk-its
|
||||
esphome/components/ds248x/* @tomwellnitz
|
||||
esphome/components/ds3231/* @linkedupbits
|
||||
esphome/components/dsmr/* @glmnet @PolarGoose
|
||||
esphome/components/duty_time/* @dudanov
|
||||
esphome/components/ecocomfort2/* @gledian
|
||||
esphome/components/ee895/* @Stock-M
|
||||
esphome/components/ektf2232/touchscreen/* @jesserockz
|
||||
esphome/components/emc2101/* @ellull
|
||||
@@ -192,7 +183,6 @@ esphome/components/esp32_camera_web_server/* @ayufan
|
||||
esphome/components/esp32_can/* @Sympatron
|
||||
esphome/components/esp32_hosted/* @swoboda1337
|
||||
esphome/components/esp32_hosted/update/* @swoboda1337
|
||||
esphome/components/esp32_power_management/* @rwrozelle
|
||||
esphome/components/esp32_rmt/* @jesserockz
|
||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||
esphome/components/esp8266/* @esphome/core
|
||||
@@ -207,14 +197,12 @@ esphome/components/exponential_moving_average/* @clydebarrow
|
||||
esphome/components/exposure_notifications/* @OttoWinter
|
||||
esphome/components/ezo/* @ssieb
|
||||
esphome/components/ezo_pmp/* @carlos-sarmiento
|
||||
esphome/components/ezsp_proxy_tap/* @kbx81 @puddly
|
||||
esphome/components/factory_reset/* @anatoly-savchenkov
|
||||
esphome/components/fastled_base/* @OttoWinter
|
||||
esphome/components/feedback/* @ianchi
|
||||
esphome/components/file/* @esphome/core
|
||||
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
|
||||
esphome/components/font/* @clydebarrow @esphome/core
|
||||
esphome/components/friedrich/* @rwrozelle
|
||||
esphome/components/fs3000/* @kahrendt
|
||||
esphome/components/ft5x06/* @clydebarrow
|
||||
esphome/components/ft63x6/* @gpambrozio
|
||||
@@ -276,7 +264,6 @@ esphome/components/http_request/ota/* @oarcher
|
||||
esphome/components/http_request/update/* @jesserockz
|
||||
esphome/components/htu31d/* @betterengineering
|
||||
esphome/components/hub75/* @stuartparmenter
|
||||
esphome/components/husb238/* @latonita
|
||||
esphome/components/hydreon_rgxx/* @functionpointer
|
||||
esphome/components/hyt271/* @Philippe12
|
||||
esphome/components/i2c/* @esphome/core
|
||||
@@ -284,7 +271,6 @@ esphome/components/i2c_device/* @gabest11
|
||||
esphome/components/i2s_audio/* @jesserockz
|
||||
esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/i2s_clockless_led_strip/* @j9brown
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
esphome/components/icnt86/* @danepowell
|
||||
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
||||
@@ -314,11 +300,9 @@ esphome/components/kuntze/* @ssieb
|
||||
esphome/components/lc709203f/* @ilikecake
|
||||
esphome/components/lcd_menu/* @numo68
|
||||
esphome/components/ld2410/* @regevbr @sebcaps
|
||||
esphome/components/ld2410s/* @NovakIrs
|
||||
esphome/components/ld2412/* @Rihan9
|
||||
esphome/components/ld2420/* @descipher
|
||||
esphome/components/ld2450/* @hareeshmu
|
||||
esphome/components/ld2460/* @jesusvallejo
|
||||
esphome/components/ld24xx/* @kbx81
|
||||
esphome/components/ld6002b/* @hepter
|
||||
esphome/components/ledc/* @OttoWinter
|
||||
@@ -327,10 +311,6 @@ esphome/components/libretiny_pwm/* @kuba2k2
|
||||
esphome/components/light/* @esphome/core
|
||||
esphome/components/lightwaverf/* @max246
|
||||
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
||||
esphome/components/lis2dw12/* @latonita
|
||||
esphome/components/lis2dw12_i2c/* @latonita
|
||||
esphome/components/lis2dw12_spi/* @latonita
|
||||
esphome/components/lis3dh/* @zebble
|
||||
esphome/components/lm75b/* @beormund
|
||||
esphome/components/ln882h_ble/* @Bl00d-B0b
|
||||
esphome/components/ln882h_ble_tracker/* @Bl00d-B0b
|
||||
@@ -338,7 +318,6 @@ esphome/components/ln882x/* @lamauny
|
||||
esphome/components/lock/* @esphome/core
|
||||
esphome/components/logger/* @esphome/core
|
||||
esphome/components/logger/select/* @clydebarrow
|
||||
esphome/components/lp5562/* @DennisGaida
|
||||
esphome/components/lps22/* @nagisa
|
||||
esphome/components/lsm6ds/* @clydebarrow
|
||||
esphome/components/ltr390/* @latonita @sjtrny
|
||||
@@ -349,9 +328,7 @@ esphome/components/m5stack_8angle/* @rnauber
|
||||
esphome/components/mapping/* @clydebarrow
|
||||
esphome/components/matrix_keypad/* @ssieb
|
||||
esphome/components/max17043/* @blacknell
|
||||
esphome/components/max17048/* @B48D81EFCC
|
||||
esphome/components/max31865/* @DAVe3283
|
||||
esphome/components/max31888/* @JiriPrchal
|
||||
esphome/components/max44009/* @berfenger
|
||||
esphome/components/max6956/* @looping40
|
||||
esphome/components/max7219digit/* @rspaargaren
|
||||
@@ -370,6 +347,7 @@ esphome/components/mcp4461/* @p1ngb4ck
|
||||
esphome/components/mcp4728/* @berfenger
|
||||
esphome/components/mcp47a1/* @jesserockz
|
||||
esphome/components/mcp9600/* @mreditor97
|
||||
esphome/components/mcp9808/* @k7hpn
|
||||
esphome/components/md5/* @esphome/core
|
||||
esphome/components/mdns/* @esphome/core
|
||||
esphome/components/media_player/* @jesserockz
|
||||
@@ -383,7 +361,6 @@ esphome/components/midea_ir/* @dudanov
|
||||
esphome/components/mipi_dsi/* @clydebarrow
|
||||
esphome/components/mipi_rgb/* @clydebarrow
|
||||
esphome/components/mipi_spi/* @clydebarrow
|
||||
esphome/components/mistral_ir/* @lerhum
|
||||
esphome/components/mitsubishi/* @RubyBailey
|
||||
esphome/components/mitsubishi_cn105/* @crnjan
|
||||
esphome/components/mixer/speaker/* @kahrendt
|
||||
@@ -422,8 +399,6 @@ esphome/components/noblex/* @AGalfra
|
||||
esphome/components/noise/* @esphome/core
|
||||
esphome/components/npi19/* @bakerkj
|
||||
esphome/components/nrf52/* @tomaszduda23
|
||||
esphome/components/ns2009/* @tchilov
|
||||
esphome/components/ns2009/touchscreen/* @tchilov
|
||||
esphome/components/number/* @esphome/core
|
||||
esphome/components/one_wire/* @ssieb
|
||||
esphome/components/online_image/* @clydebarrow @guillempages
|
||||
@@ -438,7 +413,6 @@ esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
||||
esphome/components/pcf85063/* @brogon
|
||||
esphome/components/pcf8563/* @KoenBreeman
|
||||
esphome/components/pcm5122/* @remcom
|
||||
esphome/components/pd_pioneer_ir/* @martinsah
|
||||
esphome/components/pi4ioe5v6408/* @jesserockz
|
||||
esphome/components/pid/* @OttoWinter
|
||||
esphome/components/pipsolar/* @andreashergert1984
|
||||
@@ -457,7 +431,6 @@ esphome/components/pn7160/* @jesserockz @kbx81
|
||||
esphome/components/pn7160_i2c/* @jesserockz @kbx81
|
||||
esphome/components/pn7160_spi/* @jesserockz @kbx81
|
||||
esphome/components/pn71xx/* @jesserockz @kbx81
|
||||
esphome/components/power_management/* @rwrozelle
|
||||
esphome/components/power_supply/* @esphome/core
|
||||
esphome/components/preferences/* @esphome/core
|
||||
esphome/components/provisioning/* @esphome/core
|
||||
@@ -478,8 +451,6 @@ esphome/components/rc522/* @glmnet
|
||||
esphome/components/rc522_i2c/* @glmnet
|
||||
esphome/components/rc522_spi/* @glmnet
|
||||
esphome/components/rd03d/* @jasstrong
|
||||
esphome/components/remko_ar715/* @naKruul
|
||||
esphome/components/renogy_inverter_ble/* @emilioaray-dev
|
||||
esphome/components/resampler/speaker/* @kahrendt
|
||||
esphome/components/restart/* @esphome/core
|
||||
esphome/components/rf_bridge/* @jesserockz
|
||||
@@ -499,7 +470,6 @@ esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8025t/* @remcom
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
esphome/components/samsung/* @jorofi
|
||||
esphome/components/scd4x/* @martgras @sjtrny
|
||||
esphome/components/script/* @esphome/core
|
||||
esphome/components/sdl/* @bdm310 @clydebarrow
|
||||
@@ -528,14 +498,11 @@ esphome/components/sfa30/* @ghsensdev
|
||||
esphome/components/sfa40/* @NoQuarrel
|
||||
esphome/components/sgp40/* @SenexCrenshaw
|
||||
esphome/components/sgp4x/* @martgras @SenexCrenshaw
|
||||
esphome/components/sh1122_base/* @pl4nkton
|
||||
esphome/components/sh1122_spi/* @pl4nkton
|
||||
esphome/components/sha256/* @esphome/core
|
||||
esphome/components/shelly_dimmer/* @edge90 @rnauber
|
||||
esphome/components/sht3xd/* @mrtoy-me
|
||||
esphome/components/sht4x/* @sjtrny
|
||||
esphome/components/shutdown/* @esphome/core @jsuanet
|
||||
esphome/components/siemens_ira211/* @fonix232
|
||||
esphome/components/sigma_delta_output/* @Cat-Ion
|
||||
esphome/components/sim800l/* @glmnet
|
||||
esphome/components/sm10bit_base/* @Cossid
|
||||
@@ -598,13 +565,12 @@ esphome/components/t6615/* @tylermenezes
|
||||
esphome/components/tas2780/* @remcom
|
||||
esphome/components/tas58xx/* @mrtoy-me @remcom
|
||||
esphome/components/tc74/* @sethgirvan
|
||||
esphome/components/tca8418/* @zebble
|
||||
esphome/components/tca9548a/* @andreashergert1984
|
||||
esphome/components/tca9555/* @mobrembski
|
||||
esphome/components/tcl112/* @glmnet
|
||||
esphome/components/tcp_uart/* @Bascht74
|
||||
esphome/components/tee501/* @Stock-M
|
||||
esphome/components/teleinfo/* @0hax @FredM67
|
||||
esphome/components/teleinfo/* @0hax
|
||||
esphome/components/tem3200/* @bakerkj
|
||||
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
|
||||
esphome/components/template/datetime/* @rfdarter
|
||||
@@ -616,15 +582,12 @@ esphome/components/thermopro_ble/* @sittner
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @esphome/core
|
||||
esphome/components/tinyusb/* @kbx81
|
||||
esphome/components/tinyusb_keyboard/* @frederikbolding
|
||||
esphome/components/tlc5947/* @rnauber
|
||||
esphome/components/tlc5971/* @IJIJI
|
||||
esphome/components/tm1621/* @Philippe12
|
||||
esphome/components/tm1637/* @glmnet
|
||||
esphome/components/tm1638/* @skykingjwc
|
||||
esphome/components/tm1651/* @mrtoy-me
|
||||
esphome/components/tmc2209/* @remcom
|
||||
esphome/components/tmc22xx/* @remcom
|
||||
esphome/components/tmp102/* @timsavage
|
||||
esphome/components/tmp1075/* @sybrenstuvel
|
||||
esphome/components/tmp117/* @Azimath
|
||||
@@ -676,7 +639,7 @@ esphome/components/web_server_idf/* @dentra
|
||||
esphome/components/weikai/* @DrCoolZic
|
||||
esphome/components/weikai_i2c/* @DrCoolZic
|
||||
esphome/components/weikai_spi/* @DrCoolZic
|
||||
esphome/components/whirlpool/* @arpena @glmnet
|
||||
esphome/components/whirlpool/* @glmnet
|
||||
esphome/components/whynter/* @aeonsablaze
|
||||
esphome/components/wiegand/* @ssieb
|
||||
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.1
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
+15
-181
@@ -48,13 +48,9 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WIFI,
|
||||
ENV_NOGITIGNORE,
|
||||
KEY_CORE,
|
||||
KEY_ESP32,
|
||||
KEY_TARGET_PLATFORM,
|
||||
KEY_VARIANT,
|
||||
PLATFORM_HOST,
|
||||
SECRETS_FILES,
|
||||
ErrorFormat,
|
||||
Toolchain,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError, coroutine
|
||||
@@ -73,7 +69,6 @@ from esphome.util import (
|
||||
get_serial_ports,
|
||||
is_picotool_usb_permission_error,
|
||||
list_yaml_files,
|
||||
read_secret_line,
|
||||
run_external_command,
|
||||
run_external_process,
|
||||
safe_print,
|
||||
@@ -1236,28 +1231,14 @@ def upload_program(
|
||||
config: ConfigType, args: ArgsProtocol, devices: list[str]
|
||||
) -> tuple[int, str | None]:
|
||||
host = devices[0]
|
||||
port_type = get_port_type(host)
|
||||
ota_key = getattr(args, "ota_key", None)
|
||||
if ota_key and port_type in (PortType.SERIAL, PortType.BOOTSEL):
|
||||
_LOGGER.warning(
|
||||
"The presented OTA key only applies to network uploads; ignored for %s",
|
||||
host,
|
||||
)
|
||||
|
||||
platform_upload = platform_hooks.get_platform_hook(
|
||||
CORE.target_platform, "upload_program"
|
||||
)
|
||||
if platform_upload is not None and platform_upload(config, args, host):
|
||||
if ota_key and port_type not in (PortType.SERIAL, PortType.BOOTSEL):
|
||||
# A platform's own network path (nrf52 mcumgr) has no key handshake
|
||||
_LOGGER.warning(
|
||||
"The presented OTA key only applies to the %s OTA platform; "
|
||||
"ignored for %s",
|
||||
CONF_ESPHOME,
|
||||
host,
|
||||
)
|
||||
return 0, host
|
||||
|
||||
port_type = get_port_type(host)
|
||||
|
||||
# MQTT and MQTTIP are also OTA paths; MQTTIP gets resolved to a real IP later by
|
||||
# _resolve_network_devices(). Only SERIAL and BOOTSEL are non-OTA upload paths.
|
||||
is_partition_table = getattr(args, "partition_table", False)
|
||||
@@ -1300,8 +1281,6 @@ def upload_program(
|
||||
network_devices = _resolve_network_devices(devices, config, args)
|
||||
|
||||
if chosen_platform == CONF_WEB_SERVER:
|
||||
if ota_key:
|
||||
raise EsphomeError(OTA_KEY_PLATFORM_ERROR)
|
||||
if is_partition_table or is_bootloader:
|
||||
raise EsphomeError(
|
||||
f"{option_string} is only supported with the esphome OTA platform; "
|
||||
@@ -1315,17 +1294,6 @@ def upload_program(
|
||||
return _upload_via_native_api(config, network_devices, args)
|
||||
|
||||
|
||||
def _ota_upload_platforms(config: ConfigType) -> dict[str, None]:
|
||||
"""The network OTA platforms the config provides, one entry each, in
|
||||
config order; the web_server final-validate hook merges duplicates."""
|
||||
return {
|
||||
ota_platform: None
|
||||
for ota_item in config.get(CONF_OTA, [])
|
||||
if (ota_platform := ota_item.get(CONF_PLATFORM))
|
||||
in (CONF_ESPHOME, CONF_WEB_SERVER)
|
||||
}
|
||||
|
||||
|
||||
def _choose_ota_platform(config: ConfigType, requested: str | None) -> str:
|
||||
"""Pick the OTA platform to use, optionally honoring ``--ota-platform``.
|
||||
|
||||
@@ -1338,7 +1306,16 @@ def _choose_ota_platform(config: ConfigType, requested: str | None) -> str:
|
||||
stream on ESP32/LibreTiny, which inflate it as it arrives.) Falls back to
|
||||
``web_server`` only when that is the only available platform.
|
||||
"""
|
||||
available = _ota_upload_platforms(config)
|
||||
# Use a dict (insertion-ordered) instead of a list so error messages and
|
||||
# membership checks see one entry per platform even if the user has
|
||||
# multiple ``ota:`` items of the same platform; the web_server OTA
|
||||
# platform's final-validate hook merges duplicates anyway.
|
||||
available: dict[str, None] = {}
|
||||
for ota_item in config.get(CONF_OTA, []):
|
||||
ota_platform = ota_item.get(CONF_PLATFORM)
|
||||
if ota_platform in (CONF_ESPHOME, CONF_WEB_SERVER):
|
||||
available[ota_platform] = None
|
||||
|
||||
if not available:
|
||||
raise EsphomeError(
|
||||
f"Cannot upload Over the Air as the {CONF_OTA} configuration is not "
|
||||
@@ -1376,15 +1353,9 @@ def _upload_via_native_api(
|
||||
# Fail closed: an encryption block whose key did not resolve must never
|
||||
# fall back to a plaintext upload
|
||||
noise_psk = None
|
||||
old_noise_psk = None
|
||||
plaintext_fallback = False
|
||||
allow_plaintext_upload = False
|
||||
if ota_key := getattr(args, "ota_key", None):
|
||||
# The user presents a key the device still runs instead of the
|
||||
# configured one (a key change in progress); an explicit key never
|
||||
# downgrades to plaintext
|
||||
noise_psk = ota_key
|
||||
elif (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
|
||||
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
|
||||
noise_psk = encryption_conf.get(CONF_KEY)
|
||||
allow_plaintext_upload = bool(
|
||||
encryption_conf.get(espota2.CONF_ALLOW_PLAINTEXT_UPLOAD)
|
||||
@@ -1397,8 +1368,6 @@ def _upload_via_native_api(
|
||||
# Ensure the key is a string, as required by the underlying OTA implementation.
|
||||
# It arrives here as a SensitiveStr which aioesphomeapi rejects.
|
||||
noise_psk = str(noise_psk)
|
||||
if old_key := encryption_conf.get(espota2.CONF_OLD_KEY):
|
||||
old_noise_psk = str(old_key)
|
||||
elif api_key := static_encryption_key(config.get(CONF_API) or {}):
|
||||
# Remove before 2027.3.0: the api key is tried, falling back to plaintext
|
||||
noise_psk = str(api_key)
|
||||
@@ -1448,22 +1417,9 @@ def _upload_via_native_api(
|
||||
noise_psk,
|
||||
plaintext_fallback=plaintext_fallback,
|
||||
allow_plaintext_upload=allow_plaintext_upload,
|
||||
old_noise_psk=old_noise_psk,
|
||||
)
|
||||
|
||||
|
||||
def _old_key_notice(config: ConfigType) -> None:
|
||||
"""After `run`, whose image was just built from this config; a plain
|
||||
`upload` may send a stale image that still carries the previous key."""
|
||||
from esphome import espota2
|
||||
|
||||
for item in config.get(CONF_OTA, []):
|
||||
if item.get(CONF_PLATFORM) == CONF_ESPHOME and (
|
||||
item.get(CONF_ENCRYPTION) or {}
|
||||
).get(espota2.CONF_OLD_KEY):
|
||||
_LOGGER.warning(espota2.OLD_KEY_REMOVE_NOTICE)
|
||||
|
||||
|
||||
def _upload_via_web_server(
|
||||
config: ConfigType, network_devices: list[str], binary: Path
|
||||
) -> tuple[int, str | None]:
|
||||
@@ -1781,60 +1737,7 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
return 0
|
||||
|
||||
|
||||
ENV_OTA_KEY = "ESPHOME_OTA_KEY"
|
||||
OTA_KEY_PLATFORM_ERROR = (
|
||||
f"--prompt-ota-key and {ENV_OTA_KEY} only apply to the {CONF_ESPHOME} OTA "
|
||||
"platform; to flash by serial instead, name the port with --device"
|
||||
)
|
||||
|
||||
|
||||
def _read_ota_key(args: ArgsProtocol, config: ConfigType) -> None:
|
||||
"""Take the key to present to the device for this upload, from the
|
||||
prompt or from ESPHOME_OTA_KEY, before any compile so the prompt is
|
||||
not buried in build output."""
|
||||
prompt = getattr(args, "prompt_ota_key", False)
|
||||
env_key = getattr(args, "env_ota_key", None)
|
||||
if not prompt and env_key is None:
|
||||
return
|
||||
if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_HOST:
|
||||
_LOGGER.warning("A host program runs locally; the presented OTA key is ignored")
|
||||
return
|
||||
# The HTTP path has no key handshake; decide before prompting or
|
||||
# compiling. A serial target is a serial flash: the key is not even
|
||||
# asked for, and --ota-platform means nothing to it. Every entry must
|
||||
# be serial: a missing port is skipped and the next entry may be a host
|
||||
devices = getattr(args, "device", None) or []
|
||||
serial_target = bool(devices) and all(
|
||||
device == "SERIAL"
|
||||
or get_port_type(device) in (PortType.SERIAL, PortType.BOOTSEL)
|
||||
for device in devices
|
||||
)
|
||||
chosen = (
|
||||
_choose_ota_platform(config, getattr(args, "ota_platform", None))
|
||||
if not serial_target and _ota_upload_platforms(config)
|
||||
else None
|
||||
)
|
||||
if chosen == CONF_WEB_SERVER:
|
||||
raise EsphomeError(OTA_KEY_PLATFORM_ERROR)
|
||||
from esphome.components.noise import validate_encryption_key
|
||||
from esphome.config_validation import Invalid
|
||||
|
||||
if serial_target:
|
||||
_LOGGER.warning(
|
||||
"The presented OTA key only applies to network uploads; ignored for %s",
|
||||
", ".join(devices),
|
||||
)
|
||||
return
|
||||
key = read_secret_line("OTA encryption key: ") if prompt else env_key
|
||||
try:
|
||||
validate_encryption_key(key)
|
||||
except Invalid as err:
|
||||
raise EsphomeError(f"Invalid OTA encryption key: {err}") from err
|
||||
args.ota_key = key
|
||||
|
||||
|
||||
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
_read_ota_key(args, config)
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device,
|
||||
@@ -1882,7 +1785,6 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Elsewhere the flag is ignored, so serial stays fine there.
|
||||
_LOGGER.error("--skip-bootloader builds cannot be flashed over serial")
|
||||
return 1
|
||||
_read_ota_key(args, config)
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -1908,7 +1810,6 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
exit_code, successful_device = upload_program(config, args, devices)
|
||||
if exit_code == 0:
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
_old_key_notice(config)
|
||||
else:
|
||||
_LOGGER.warning("Failed to upload to %s", devices)
|
||||
return exit_code
|
||||
@@ -2304,25 +2205,6 @@ def _add_states_args(parser: argparse.ArgumentParser) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _error_format(value: str) -> ErrorFormat:
|
||||
try:
|
||||
return ErrorFormat(value.lower())
|
||||
except ValueError:
|
||||
choices = ", ".join(ErrorFormat)
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"invalid choice: '{value}' (choose from {choices})"
|
||||
) from None
|
||||
|
||||
|
||||
ARGUMENT_HELP_PROMPT_OTA_KEY = (
|
||||
"Ask for the OTA encryption key to present to the device instead of using "
|
||||
"the one in the configuration, for a device that still runs a previous key. "
|
||||
"The key is read without echo, or as one line from stdin when that is not "
|
||||
"a terminal; it is never taken from the command line. A calling program "
|
||||
f"can hand the key over in the {ENV_OTA_KEY} environment variable instead."
|
||||
)
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
options_parser = argparse.ArgumentParser(add_help=False)
|
||||
options_parser.add_argument(
|
||||
@@ -2385,17 +2267,6 @@ def parse_args(argv):
|
||||
),
|
||||
)
|
||||
|
||||
options_parser.add_argument(
|
||||
"--error-format",
|
||||
type=_error_format,
|
||||
metavar="{" + ",".join(f.value for f in ErrorFormat) + "}",
|
||||
help=(
|
||||
"How configuration errors are shown. 'yaml' shows them inside the "
|
||||
"failing YAML; 'line' prints one 'file:line:column: error: message' "
|
||||
"line per error. Default: $ESPHOME_ERROR_FORMAT, else 'yaml'."
|
||||
),
|
||||
)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=f"ESPHome {const.__version__}", parents=[options_parser]
|
||||
)
|
||||
@@ -2491,11 +2362,6 @@ def parse_args(argv):
|
||||
help="Upload as partition table (OTA).",
|
||||
action="store_true",
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--prompt-ota-key",
|
||||
action="store_true",
|
||||
help=ARGUMENT_HELP_PROMPT_OTA_KEY,
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--bootloader",
|
||||
help="Upload as bootloader (OTA).",
|
||||
@@ -2576,11 +2442,6 @@ def parse_args(argv):
|
||||
"(HTTP Basic auth) when that is the only configured platform."
|
||||
),
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--prompt-ota-key",
|
||||
action="store_true",
|
||||
help=ARGUMENT_HELP_PROMPT_OTA_KEY,
|
||||
)
|
||||
|
||||
parser_clean = subparsers.add_parser(
|
||||
"clean-mqtt",
|
||||
@@ -2709,18 +2570,9 @@ def parse_args(argv):
|
||||
args, unknown_args = parser.parse_known_args(arguments)
|
||||
if unknown_args:
|
||||
_LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
|
||||
else:
|
||||
args = parser.parse_args(arguments)
|
||||
return args
|
||||
|
||||
# Read here rather than as the option default, so a bad value is reported
|
||||
# against the environment variable instead of --error-format
|
||||
if args.error_format is None:
|
||||
env_value = os.getenv("ESPHOME_ERROR_FORMAT", ErrorFormat.YAML.value)
|
||||
try:
|
||||
args.error_format = _error_format(env_value)
|
||||
except argparse.ArgumentTypeError as err:
|
||||
parser.error(f"ESPHOME_ERROR_FORMAT: {err}")
|
||||
return args
|
||||
return parser.parse_args(arguments)
|
||||
|
||||
|
||||
def _warn_if_source_tree_mismatch() -> None:
|
||||
@@ -2793,7 +2645,6 @@ def run_esphome(argv):
|
||||
args = parse_args(argv)
|
||||
CORE.dashboard = args.dashboard
|
||||
CORE.testing_mode = args.testing_mode
|
||||
CORE.error_format = args.error_format
|
||||
|
||||
# Create address cache from command-line arguments
|
||||
CORE.address_cache = AddressCache.from_cli_args(
|
||||
@@ -2806,23 +2657,6 @@ def run_esphome(argv):
|
||||
args.log_level = "CRITICAL"
|
||||
|
||||
setup_log(log_level=args.log_level)
|
||||
# Taken out of the environment before any command runs, so no build
|
||||
# tool inherits a key a calling program handed over
|
||||
args.env_ota_key = os.environ.pop(ENV_OTA_KEY, None)
|
||||
if args.command in ("upload", "run"):
|
||||
# One key is one device's; several configs run in child processes
|
||||
# that would never see it
|
||||
if (args.env_ota_key is not None or args.prompt_ota_key) and len(
|
||||
args.configuration
|
||||
) > 1:
|
||||
raise EsphomeError(
|
||||
f"--prompt-ota-key and {ENV_OTA_KEY} take one configuration; "
|
||||
f"{len(args.configuration)} were given"
|
||||
)
|
||||
elif args.env_ota_key is not None:
|
||||
_LOGGER.warning(
|
||||
"%s is set but the %s command does not use it", ENV_OTA_KEY, args.command
|
||||
)
|
||||
_warn_if_source_tree_mismatch()
|
||||
_warn_if_intel_macos()
|
||||
|
||||
|
||||
+23
-118
@@ -1,4 +1,4 @@
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
import string
|
||||
@@ -248,23 +248,6 @@ def flash_string(config: ConfigType, value: str) -> str:
|
||||
return str(cg.progmem_string(value))
|
||||
|
||||
|
||||
def flash_compare(config: ConfigType, value: str) -> str:
|
||||
"""Default renderer for ``std::string`` constants in conditions; compares in flash on ESP8266."""
|
||||
if CORE.is_esp8266:
|
||||
return f"::esphome::ProgmemStringRef{{PSTR({cg.safe_exp(value)}), {len(value.encode('utf-8'))}}}"
|
||||
return str(cg.safe_exp(value))
|
||||
|
||||
|
||||
def _default_std_string(
|
||||
members: list[tuple[str, Any, Any]], renderer: Callable[[ConfigType, str], str]
|
||||
) -> list[tuple[str, Any, Any]]:
|
||||
"""Give ``cg.std_string`` members without a renderer the given default."""
|
||||
return [
|
||||
(key, t, fn or (renderer if t is cg.std_string else None))
|
||||
for key, t, fn in members
|
||||
]
|
||||
|
||||
|
||||
def literal_with_length(config: ConfigType, value: str) -> str:
|
||||
"""Renderer for a ``(const char *, size_t)`` target: a plain literal plus its byte length.
|
||||
|
||||
@@ -482,22 +465,6 @@ def register_apply_action(
|
||||
) -> None:
|
||||
"""Register an action that only forwards config values to its parent, with no C++ class.
|
||||
|
||||
See ``apply_action_builder``.
|
||||
"""
|
||||
register_action(name, ApplyAction, schema, synchronous=True)(
|
||||
apply_action_builder(name, schema, *fields, call=call, id_key=id_key)
|
||||
)
|
||||
|
||||
|
||||
def apply_action_builder(
|
||||
name: str,
|
||||
schema: cv.Schema,
|
||||
*fields: ApplyField | ApplyCall,
|
||||
call: str | None = None,
|
||||
id_key: str = CONF_ID,
|
||||
) -> Callable[..., Awaitable[MockObj]]:
|
||||
"""The builder ``register_apply_action`` registers, for a caller that wraps it.
|
||||
|
||||
Generates one static function with the parent (read from ``id_key``) and constants baked
|
||||
in, lambdas called inline with the trigger args, and an ``ApplyAction`` templated on it.
|
||||
A constant that is an id (``cv.use_id`` under ``cv.templatable``) is the object it names.
|
||||
@@ -508,7 +475,10 @@ def apply_action_builder(
|
||||
statements_spec = [
|
||||
(
|
||||
c.target,
|
||||
_default_std_string(c.members, flash_string),
|
||||
[
|
||||
(key, t, fn or (flash_string if t is cg.std_string else None))
|
||||
for key, t, fn in c.members
|
||||
],
|
||||
)
|
||||
for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields)
|
||||
]
|
||||
@@ -545,7 +515,7 @@ def apply_action_builder(
|
||||
action_id, cg.void, template_arg, lambda_args, statements
|
||||
)
|
||||
|
||||
return builder
|
||||
register_action(name, ApplyAction, schema, synchronous=True)(builder)
|
||||
|
||||
|
||||
def _apply_call_active(members: list[tuple[Any, Any, Any]], values: list[Any]) -> bool:
|
||||
@@ -577,12 +547,11 @@ def register_apply_condition(
|
||||
``check`` is applied to the parent: ``"is_playing()"`` becomes ``parent->is_playing()``; an
|
||||
``ApplyCall`` such as ``ApplyCall("state == {}", ((CONF_STATE, cg.bool_),))`` compares
|
||||
against config values, all of which must be present. Write ``== false`` to negate.
|
||||
A ``cg.std_string`` constant stays in flash on ESP8266 and supports only ``==`` or ``!=``
|
||||
against a ``std::string`` or ``StringRef`` member; other string types are plain literals.
|
||||
String constants are plain literals, so compare a ``std::string`` or ``StringRef`` member.
|
||||
Generates one static predicate and an ``ApplyCondition`` templated on it.
|
||||
"""
|
||||
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
|
||||
members = _default_std_string(call.members, flash_compare)
|
||||
members = call.members
|
||||
_check_key_in_schema(name, schema, id_key)
|
||||
for conf_key, _, _ in members:
|
||||
_check_key_in_schema(name, schema, conf_key)
|
||||
@@ -755,7 +724,7 @@ async def condition_group_to_code(
|
||||
if len(conditions) == 1:
|
||||
return conditions[0]
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), *conditions
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@@ -871,13 +840,11 @@ async def if_action_to_code(
|
||||
condition = await build_condition(config[cond_conf], template_arg, args)
|
||||
var = cg.new_Pvariable(action_id, if_template_arg, condition)
|
||||
if CONF_THEN in config:
|
||||
for action in await build_action_list(config[CONF_THEN], template_arg, args):
|
||||
cg.add(var.add_then_action(action))
|
||||
cg.add(var.finish_then())
|
||||
actions = await build_action_list(config[CONF_THEN], template_arg, args)
|
||||
cg.add(var.add_then(actions))
|
||||
if has_else:
|
||||
for action in await build_action_list(config[CONF_ELSE], template_arg, args):
|
||||
cg.add(var.add_else_action(action))
|
||||
cg.add(var.finish_else())
|
||||
actions = await build_action_list(config[CONF_ELSE], template_arg, args)
|
||||
cg.add(var.add_else(actions))
|
||||
return var
|
||||
|
||||
|
||||
@@ -900,9 +867,8 @@ async def while_action_to_code(
|
||||
) -> MockObj:
|
||||
condition = await build_condition(config[CONF_CONDITION], template_arg, args)
|
||||
var = cg.new_Pvariable(action_id, template_arg, condition)
|
||||
for action in await build_action_list(config[CONF_THEN], template_arg, args):
|
||||
cg.add(var.add_then_action(action))
|
||||
cg.add(var.finish_then())
|
||||
actions = await build_action_list(config[CONF_THEN], template_arg, args)
|
||||
cg.add(var.add_then(actions))
|
||||
return var
|
||||
|
||||
|
||||
@@ -931,9 +897,7 @@ async def repeat_action_to_code(
|
||||
cg.TemplateArguments(cg.uint32, *template_arg.args),
|
||||
[(cg.uint32, "iteration"), *args],
|
||||
)
|
||||
for action in actions:
|
||||
cg.add(var.add_then_action(action))
|
||||
cg.add(var.finish_then())
|
||||
cg.add(var.add_then(actions))
|
||||
return var
|
||||
|
||||
|
||||
@@ -1069,79 +1033,21 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool:
|
||||
Non-synchronous actions (delay, wait_until, script.wait, etc.) store
|
||||
trigger args for later execution, making non-owning types like StringRef
|
||||
unsafe.
|
||||
|
||||
Accepts an action list, a single action, or an automation. Only the keys of
|
||||
action entries are looked up as action names; the options of an action are
|
||||
only searched for nested action lists (``then:``, ``else:``, ...), so an
|
||||
option that shares a name with an action, such as the ``delay:`` option of
|
||||
``speaker_source.set_playlist_delay``, is not mistaken for that action.
|
||||
"""
|
||||
if isinstance(actions, list):
|
||||
return any(has_non_synchronous_actions(item) for item in actions)
|
||||
if isinstance(actions, dict):
|
||||
for key, options in actions.items():
|
||||
for key in actions:
|
||||
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
|
||||
return True
|
||||
if _options_have_non_synchronous_actions(options):
|
||||
return True
|
||||
return any(
|
||||
has_non_synchronous_actions(v)
|
||||
for v in actions.values()
|
||||
if isinstance(v, (list, dict))
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _options_have_non_synchronous_actions(options: Any) -> bool:
|
||||
"""Search an action's options for nested action lists, without treating option keys as actions."""
|
||||
if isinstance(options, list):
|
||||
return has_non_synchronous_actions(options)
|
||||
if isinstance(options, dict):
|
||||
return any(_options_have_non_synchronous_actions(v) for v in options.values())
|
||||
return False
|
||||
|
||||
|
||||
def validate_no_deferring_actions(
|
||||
context: str, data: str = "the trigger data"
|
||||
) -> Callable[[ConfigType], ConfigType]:
|
||||
"""Return a validator that rejects deferring actions in a validated automation.
|
||||
|
||||
Use it for triggers whose arguments only live while the callback runs (views into a reused
|
||||
buffer, a JSON document freed on return, ...): deferring actions keep the arguments for later.
|
||||
|
||||
:param context: Where the automation is, for the error message, e.g. ``"on_json and on_data"``.
|
||||
:param data: What is only valid while the automation runs, for the error message.
|
||||
"""
|
||||
|
||||
def validator(value: ConfigType) -> ConfigType:
|
||||
if has_non_synchronous_actions(value):
|
||||
raise cv.Invalid(
|
||||
f"Deferring actions (delay, wait_until, script.wait, ...) are not allowed in "
|
||||
f"{context}: {data} is only valid while the automation runs. Copy what you need "
|
||||
"into globals first, then defer in a separate script or automation."
|
||||
)
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def validate_synchronous_automation(
|
||||
context: str,
|
||||
data: str = "the trigger data",
|
||||
extra_schema=None,
|
||||
extra_validators=None,
|
||||
single: bool = False,
|
||||
) -> cv.All:
|
||||
"""``validate_automation()`` that also rejects deferring actions.
|
||||
|
||||
See ``validate_no_deferring_actions()`` for ``context`` and ``data``; the other parameters are
|
||||
passed to ``validate_automation()``.
|
||||
"""
|
||||
return cv.All(
|
||||
validate_automation(
|
||||
extra_schema=extra_schema,
|
||||
extra_validators=extra_validators,
|
||||
single=single,
|
||||
),
|
||||
validate_no_deferring_actions(context, data),
|
||||
)
|
||||
|
||||
|
||||
async def _new_automation(
|
||||
args: TemplateArgsType, config: ConfigType, *ctor_args: MockObj
|
||||
) -> MockObj:
|
||||
@@ -1149,8 +1055,7 @@ async def _new_automation(
|
||||
templ = cg.TemplateArguments(*(arg[0] for arg in args))
|
||||
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, *ctor_args)
|
||||
actions = await build_action_list(config[CONF_THEN], templ, args)
|
||||
for action in actions:
|
||||
cg.add(obj.add_action(action))
|
||||
cg.add(obj.add_actions(actions))
|
||||
return obj
|
||||
|
||||
|
||||
|
||||
@@ -1057,7 +1057,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
cxx = (toolchain_tool(paths.toolchain, "g++"),)
|
||||
lines = [
|
||||
*tool_lines((toolchain_tool(paths.toolchain, "gcc"),), cxx, ccache),
|
||||
*compile_rule_lines(ccache),
|
||||
*compile_rule_lines(),
|
||||
*ar_rule_lines(toolchain_tool(paths.toolchain, "ar")),
|
||||
*pch_rule_lines(),
|
||||
"rule link",
|
||||
|
||||
@@ -240,7 +240,7 @@ def write_project(compilers: HostCompilers, ccache: str | None) -> bool:
|
||||
|
||||
lines = [
|
||||
*tool_lines(compilers.cc, compilers.cxx, ccache),
|
||||
*compile_rule_lines(ccache),
|
||||
*compile_rule_lines(),
|
||||
*pch_rule_lines(),
|
||||
"rule link",
|
||||
" command = $cxx -o $out $linkflags @$out.rsp $archives $libdirflags $libflags",
|
||||
|
||||
@@ -74,40 +74,33 @@ def tool_lines(
|
||||
# path gets
|
||||
f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
|
||||
f"buildtool = {_q(_BUILD_TOOL)}",
|
||||
*([f"ccache = {_q(ccache)}"] if ccache else []),
|
||||
f"ccache = {_q(ccache) if ccache else ''}",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
def compile_rule_lines(ccache: str | None) -> list[str]:
|
||||
"""The compile rules; ``$own_includes`` is empty unless an edge sets it.
|
||||
|
||||
``ccache`` is the launcher ``tool_lines`` was given; the same test here
|
||||
keeps the variable and the rules in step. It is spliced in rather than
|
||||
left as an empty variable: a leading space on the command line makes
|
||||
CreateProcess fail on Windows.
|
||||
"""
|
||||
launcher = "$ccache " if ccache else ""
|
||||
def compile_rule_lines() -> list[str]:
|
||||
"""The compile rules; ``$own_includes`` is empty unless an edge sets it."""
|
||||
return [
|
||||
"rule c",
|
||||
f" command = {launcher}$cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
|
||||
" command = $ccache $cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = CC $out",
|
||||
"rule cxx",
|
||||
f" command = {launcher}$cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
|
||||
" command = $ccache $cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = CXX $out",
|
||||
"rule aspp",
|
||||
f" command = {launcher}$cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
|
||||
" command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = AS $out",
|
||||
# Plain assembler, as SCons's ASCOM: no preprocessor, so no
|
||||
# depfile and no $flags (defines/includes) either
|
||||
"rule asm",
|
||||
f" command = {launcher}$cc -x assembler $asflags -c $in -o $out",
|
||||
" command = $ccache $cc -x assembler $asflags -c $in -o $out",
|
||||
" description = AS $out",
|
||||
]
|
||||
|
||||
|
||||
+12
-20
@@ -102,21 +102,25 @@ _KNOWN_FILE_EXTENSIONS = frozenset(
|
||||
)
|
||||
|
||||
|
||||
# Matches !secret references in YAML text. An optional surrounding
|
||||
# quote pair around the key is allowed and ignored: YAML treats
|
||||
# ``!secret 'foo'`` and ``!secret foo`` as the same key. This is
|
||||
# intentionally a simple regex scan rather than a YAML parse — it may
|
||||
# match inside comments or multi-line strings, which is the conservative
|
||||
# direction (include more secrets rather than fewer).
|
||||
_SECRET_RE = re.compile(r"""!secret\s+['"]?([^\s'"]+)""")
|
||||
|
||||
|
||||
def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]:
|
||||
"""Scan YAML files for ``!secret <key>`` references."""
|
||||
keys: set[str] = set()
|
||||
for fpath in yaml_files:
|
||||
try:
|
||||
text = fpath.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError) as err:
|
||||
_LOGGER.warning(
|
||||
"Could not scan %s for !secret references (%s); the bundled "
|
||||
"secret set may be incomplete",
|
||||
fpath,
|
||||
err,
|
||||
)
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
keys |= yaml_util.find_secret_references(text)
|
||||
for match in _SECRET_RE.finditer(text):
|
||||
keys.add(match.group(1))
|
||||
return keys
|
||||
|
||||
|
||||
@@ -418,18 +422,6 @@ class ConfigBundleCreator:
|
||||
must ship every candidate so the remote build can pick any one.
|
||||
"""
|
||||
discovered = yaml_util.discover_user_yaml_files(self._config_path)
|
||||
if discovered.load_errors:
|
||||
_LOGGER.warning(
|
||||
"Bundle may be incomplete; could not load all configuration files: %s",
|
||||
"; ".join(discovered.load_errors),
|
||||
)
|
||||
if discovered.unresolved:
|
||||
_LOGGER.warning(
|
||||
"Bundle may be incomplete; %d !include path(s) use "
|
||||
"substitutions and cannot be captured: %s",
|
||||
len(discovered.unresolved),
|
||||
", ".join(discovered.unresolved),
|
||||
)
|
||||
self._secrets_paths.update(discovered.secrets)
|
||||
# A !secret inside a file this re-parse does not reach (for example
|
||||
# a git-fetched package the builder re-fetches) still resolves
|
||||
|
||||
@@ -44,7 +44,6 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
safe_exp,
|
||||
set_cpp_standard,
|
||||
shared_progmem_array,
|
||||
shared_progmem_string,
|
||||
statement,
|
||||
static_const_array,
|
||||
static_function,
|
||||
|
||||
@@ -128,10 +128,10 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
|
||||
#endif // USE_RP2
|
||||
|
||||
protected:
|
||||
uint8_t sample_count_{1}; // Must match DEFAULT_SAMPLES in sensor.py
|
||||
bool output_raw_{false}; // Must match CONF_RAW default in sensor.py
|
||||
uint8_t sample_count_{1};
|
||||
bool output_raw_{false};
|
||||
InternalGPIOPin *pin_;
|
||||
SamplingMode sampling_mode_{SamplingMode::AVG}; // Must match DEFAULT_SAMPLING_MODE in sensor.py
|
||||
SamplingMode sampling_mode_{SamplingMode::AVG};
|
||||
|
||||
#ifdef USE_ESP32
|
||||
float sample_autorange_();
|
||||
@@ -139,7 +139,7 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
|
||||
bool autorange_{false};
|
||||
adc_oneshot_unit_handle_t adc_handle_{nullptr};
|
||||
adc_cali_handle_t calibration_handle_{nullptr};
|
||||
adc_atten_t attenuation_{ADC_ATTEN_DB_0}; // Must match DEFAULT_ATTENUATION in sensor.py
|
||||
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
|
||||
adc_channel_t channel_{};
|
||||
adc_unit_t adc_unit_{};
|
||||
struct SetupFlags {
|
||||
|
||||
@@ -48,11 +48,6 @@ AUTO_LOAD = ["voltage_sampler"]
|
||||
CONF_SAMPLES = "samples"
|
||||
CONF_SAMPLING_MODE = "sampling_mode"
|
||||
|
||||
# Keep in sync with the initializers in adc_sensor.h
|
||||
DEFAULT_ATTENUATION = "0db"
|
||||
DEFAULT_SAMPLING_MODE = "avg"
|
||||
DEFAULT_SAMPLES = 1
|
||||
|
||||
|
||||
_attenuation = cv.enum(ATTENUATION_MODES, lower=True)
|
||||
_sampling_mode = cv.enum(SAMPLING_MODES, lower=True)
|
||||
@@ -70,10 +65,7 @@ def validate_config(config: ConfigType) -> ConfigType:
|
||||
):
|
||||
raise cv.Invalid("ESP32-S31 only supports 'attenuation: 0db'")
|
||||
|
||||
if (
|
||||
config.get(CONF_ATTENUATION, None) == "auto"
|
||||
and config.get(CONF_SAMPLES, DEFAULT_SAMPLES) > 1
|
||||
):
|
||||
if config.get(CONF_ATTENUATION, None) == "auto" and config.get(CONF_SAMPLES, 1) > 1:
|
||||
raise cv.Invalid(
|
||||
"Automatic attenuation cannot be used when multisampling is set"
|
||||
)
|
||||
@@ -121,16 +113,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_PIN): validate_adc_pin,
|
||||
cv.Optional(CONF_RAW, default=False): cv.boolean,
|
||||
cv.SplitDefault(CONF_ATTENUATION, esp32=DEFAULT_ATTENUATION): cv.All(
|
||||
cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
|
||||
cv.only_on_esp32, _attenuation
|
||||
),
|
||||
cv.OnlyWith(CONF_NRF_SAADC, PLATFORM_NRF52): cv.declare_id(adc_dt_spec),
|
||||
cv.Optional(CONF_SAMPLES, default=DEFAULT_SAMPLES): cv.int_range(
|
||||
min=1, max=255
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_SAMPLING_MODE, default=DEFAULT_SAMPLING_MODE
|
||||
): _sampling_mode,
|
||||
cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s")),
|
||||
@@ -167,13 +155,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
# Skip the setters when the config matches the C++ initializers.
|
||||
if config[CONF_RAW]:
|
||||
cg.add(var.set_output_raw(True))
|
||||
if (samples := config[CONF_SAMPLES]) != DEFAULT_SAMPLES:
|
||||
cg.add(var.set_sample_count(samples))
|
||||
if (sampling_mode := config[CONF_SAMPLING_MODE]) != DEFAULT_SAMPLING_MODE:
|
||||
cg.add(var.set_sampling_mode(sampling_mode))
|
||||
cg.add(var.set_output_raw(config[CONF_RAW]))
|
||||
cg.add(var.set_sample_count(config[CONF_SAMPLES]))
|
||||
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
|
||||
|
||||
if CORE.is_esp32:
|
||||
# Re-enable ESP-IDF's ADC driver (excluded by default to save compile time)
|
||||
@@ -182,7 +166,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
if attenuation := config.get(CONF_ATTENUATION):
|
||||
if attenuation == "auto":
|
||||
cg.add(var.set_autorange(cg.global_ns.true))
|
||||
elif attenuation != DEFAULT_ATTENUATION:
|
||||
else:
|
||||
cg.add(var.set_attenuation(attenuation))
|
||||
|
||||
variant = get_esp32_variant()
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Support for ScioSense APC1 air quality sensor."""
|
||||
|
||||
DOMAIN = "apc1"
|
||||
@@ -1,352 +0,0 @@
|
||||
#include "apc1.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::apc1 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "apc1");
|
||||
|
||||
static constexpr uint8_t APC1_MEASUREMENT_OFFSET_AQI = 58;
|
||||
static constexpr uint8_t APC1_MEASUREMENT_OFFSET_ERROR_CODE = 61;
|
||||
|
||||
static constexpr uint8_t APC1_DEVICE_INFO_OFFSET_MODULE = 4;
|
||||
static constexpr uint8_t APC1_DEVICE_INFO_OFFSET_SERIAL = 10;
|
||||
static constexpr uint8_t APC1_DEVICE_INFO_OFFSET_FW = 19;
|
||||
|
||||
// Give a freshly powered sensor time to boot before the first retry
|
||||
static constexpr uint32_t APC1_COLD_BOOT_INIT_DELAY_MS = 2000;
|
||||
// Re-send the init commands at most this often while no frames arrive
|
||||
static constexpr uint32_t APC1_INIT_RETRY_INTERVAL_MS = 10000;
|
||||
// Active mode streams a frame every second; this long without one means the sensor stopped
|
||||
static constexpr uint32_t APC1_NO_DATA_TIMEOUT_MS = 15000;
|
||||
// Drop a partial frame when the bytes stop mid-way
|
||||
static constexpr uint32_t APC1_RX_IDLE_TIMEOUT_MS = 200;
|
||||
|
||||
// Frames and commands end with the 16-bit sum of every byte before it
|
||||
static uint16_t apc1_checksum(const uint8_t *data, size_t len) {
|
||||
uint16_t sum = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
void APC1Component::setup() {
|
||||
if (this->set_pin_ != nullptr) {
|
||||
this->set_pin_->setup();
|
||||
this->set_pin_->digital_write(true);
|
||||
}
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
|
||||
// Attempt initial communication immediately for warm restarts
|
||||
this->initialize_device_();
|
||||
|
||||
// Schedule delayed initialization to catch sensors during cold power-on bootup
|
||||
this->set_timeout(APC1_COLD_BOOT_INIT_DELAY_MS, [this]() { this->initialize_device_(); });
|
||||
}
|
||||
|
||||
void APC1Component::initialize_device_() {
|
||||
this->last_init_attempt_ = App.get_loop_component_start_time();
|
||||
if (this->active_mode_ && !this->idle_) {
|
||||
this->set_active_mode();
|
||||
}
|
||||
this->send_command_(APC1Command::APC1_COMMAND_READ_SENSOR_VERSION, 0x0000);
|
||||
}
|
||||
|
||||
void APC1Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "APC1:");
|
||||
LOG_PIN(" Set Pin: ", this->set_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
|
||||
LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM1.0 Standard", this->pm_1_0_std_sensor_);
|
||||
LOG_SENSOR(" ", "PM2.5 Standard", this->pm_2_5_std_sensor_);
|
||||
LOG_SENSOR(" ", "PM10.0 Standard", this->pm_10_0_std_sensor_);
|
||||
LOG_SENSOR(" ", "PM >0.3um", this->pm_0_3um_sensor_);
|
||||
LOG_SENSOR(" ", "PM >0.5um", this->pm_0_5um_sensor_);
|
||||
LOG_SENSOR(" ", "PM >1.0um", this->pm_1_0um_sensor_);
|
||||
LOG_SENSOR(" ", "PM >2.5um", this->pm_2_5um_sensor_);
|
||||
LOG_SENSOR(" ", "PM >5.0um", this->pm_5_0um_sensor_);
|
||||
LOG_SENSOR(" ", "PM >10.0um", this->pm_10_0um_sensor_);
|
||||
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
|
||||
LOG_SENSOR(" ", "eCO2", this->eco2_sensor_);
|
||||
LOG_SENSOR(" ", "AQI", this->aqi_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
LOG_SENSOR(" ", "Raw Temperature", this->raw_temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Raw Humidity", this->raw_humidity_sensor_);
|
||||
LOG_SENSOR(" ", "RS0", this->rs0_sensor_);
|
||||
LOG_SENSOR(" ", "RS2", this->rs2_sensor_);
|
||||
LOG_SENSOR(" ", "RS3", this->rs3_sensor_);
|
||||
LOG_SENSOR(" ", "Error Code", this->error_code_sensor_);
|
||||
}
|
||||
|
||||
void APC1Component::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// If in active mode and not idle, retry periodically if no valid data received
|
||||
if (this->active_mode_ && !this->idle_) {
|
||||
if (this->last_valid_frame_ == 0) {
|
||||
if (now - this->last_init_attempt_ >= APC1_INIT_RETRY_INTERVAL_MS) {
|
||||
ESP_LOGD(TAG, "Waiting for APC1 sensor to respond, sending init commands...");
|
||||
this->initialize_device_();
|
||||
}
|
||||
} else if (now - this->last_valid_frame_ > APC1_NO_DATA_TIMEOUT_MS &&
|
||||
now - this->last_init_attempt_ >= APC1_INIT_RETRY_INTERVAL_MS) {
|
||||
ESP_LOGW(TAG, "No data received from APC1 for %" PRIu32 " ms, re-initializing...", now - this->last_valid_frame_);
|
||||
this->initialize_device_();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->rx_index_ > 0 && now - this->last_transmission_ > APC1_RX_IDLE_TIMEOUT_MS) {
|
||||
ESP_LOGV(TAG, "Timed out waiting for data, resetting buffer");
|
||||
this->rx_index_ = 0;
|
||||
}
|
||||
|
||||
while (this->available()) {
|
||||
this->last_transmission_ = now;
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
|
||||
if (this->rx_index_ == 0) {
|
||||
if (byte == APC1_HEADER_1) {
|
||||
this->rx_buffer_[this->rx_index_++] = byte;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this->rx_index_ == 1) {
|
||||
if (byte == APC1_HEADER_2) {
|
||||
this->rx_buffer_[this->rx_index_++] = byte;
|
||||
} else if (byte != APC1_HEADER_1) {
|
||||
this->rx_index_ = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
this->rx_buffer_[this->rx_index_++] = byte;
|
||||
if (this->rx_index_ < APC1_FRAME_HEADER_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The payload carries at least the checksum and must fit the buffer; checking it before the sum keeps a
|
||||
// corrupt length from wrapping the total and walking past the buffer
|
||||
const uint16_t payload_len = encode_uint16(this->rx_buffer_[2], this->rx_buffer_[3]);
|
||||
if (payload_len < 2 || payload_len > this->rx_buffer_.size() - APC1_FRAME_HEADER_SIZE) {
|
||||
ESP_LOGW(TAG, "Invalid frame length: %u", payload_len);
|
||||
this->rx_index_ = 0;
|
||||
continue;
|
||||
}
|
||||
const uint8_t total_len = payload_len + APC1_FRAME_HEADER_SIZE;
|
||||
if (this->rx_index_ == total_len) {
|
||||
this->parse_frame_(total_len);
|
||||
this->rx_index_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APC1Component::parse_frame_(uint16_t total_len) {
|
||||
uint16_t expected_checksum = encode_uint16(this->rx_buffer_[total_len - 2], this->rx_buffer_[total_len - 1]);
|
||||
uint16_t calculated_checksum = apc1_checksum(this->rx_buffer_.data(), total_len - 2);
|
||||
if (expected_checksum != calculated_checksum) {
|
||||
ESP_LOGW(TAG, "APC1 checksum mismatch: calculated 0x%04X != expected 0x%04X", calculated_checksum,
|
||||
expected_checksum);
|
||||
return;
|
||||
}
|
||||
|
||||
if (total_len == APC1_FRAME_SIZE_MEASUREMENT) {
|
||||
this->parse_measurement_frame_();
|
||||
} else if (total_len == APC1_FRAME_SIZE_DEVICE_INFO) {
|
||||
this->parse_device_info_frame_();
|
||||
} else if (total_len == APC1_FRAME_SIZE_COMMAND_RESPONSE) {
|
||||
ESP_LOGD(TAG, "APC1 command response: cmd=0x%02X, data=0x%02X", this->rx_buffer_[4], this->rx_buffer_[5]);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Received frame of unknown length %u", total_len);
|
||||
}
|
||||
}
|
||||
|
||||
// One message per error bit, bit 0 first
|
||||
PROGMEM_STRING_TABLE(ErrorStrings, "Fan restart error (too many restarts)", "Fan speed low", "Photodiode fault",
|
||||
"Fan stopped / startup error", "Laser fault", "VOC sensor fault", "RHT sensor fault");
|
||||
|
||||
void APC1Component::parse_measurement_frame_() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->last_valid_frame_ = now;
|
||||
|
||||
uint8_t error_code = this->rx_buffer_[APC1_MEASUREMENT_OFFSET_ERROR_CODE];
|
||||
if (error_code != this->last_error_code_) {
|
||||
if (error_code != 0) {
|
||||
for (uint8_t bit = 0; bit < ErrorStrings::COUNT; bit++) {
|
||||
if (error_code & (1 << bit)) {
|
||||
ESP_LOGW(TAG, "APC1: %s", LOG_STR_ARG(ErrorStrings::get_log_str(bit, ErrorStrings::LAST_INDEX)));
|
||||
}
|
||||
}
|
||||
this->status_set_warning(LOG_STR("Sensor reported hardware error"));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "APC1: Hardware error cleared");
|
||||
this->status_clear_warning();
|
||||
}
|
||||
this->last_error_code_ = error_code;
|
||||
if (this->error_code_sensor_ != nullptr) {
|
||||
this->error_code_sensor_->publish_state(error_code);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t aqi = this->rx_buffer_[APC1_MEASUREMENT_OFFSET_AQI];
|
||||
if (aqi == 0) {
|
||||
if (!this->gas_warming_up_) {
|
||||
this->gas_warming_up_ = true;
|
||||
ESP_LOGI(TAG, "APC1: Gas sensor warming up (AQI=0)");
|
||||
}
|
||||
} else if (this->gas_warming_up_) {
|
||||
this->gas_warming_up_ = false;
|
||||
ESP_LOGI(TAG, "APC1: Gas sensor warm-up complete (AQI=%u)", aqi);
|
||||
}
|
||||
|
||||
// Measurement payload data (bytes 4-57): big-endian sensor readings.
|
||||
// The values below feed the debug summary, so they are always decoded; every
|
||||
// other reading is only decoded when its sensor is configured.
|
||||
const auto u16 = [this](uint8_t offset) {
|
||||
return encode_uint16(this->rx_buffer_[offset], this->rx_buffer_[offset + 1]);
|
||||
};
|
||||
const auto u32 = [this](uint8_t offset) {
|
||||
return encode_uint32(this->rx_buffer_[offset], this->rx_buffer_[offset + 1], this->rx_buffer_[offset + 2],
|
||||
this->rx_buffer_[offset + 3]);
|
||||
};
|
||||
uint16_t pm_1_0 = u16(10);
|
||||
uint16_t pm_2_5 = u16(12);
|
||||
uint16_t pm_10_0 = u16(14);
|
||||
uint16_t tvoc = u16(28);
|
||||
uint16_t eco2 = u16(30);
|
||||
float temp_comp = static_cast<int16_t>(u16(34)) / 10.0f;
|
||||
float hum_comp = u16(36) / 10.0f;
|
||||
|
||||
ESP_LOGD(TAG, "APC1: PM1.0: %u, PM2.5: %u, PM10: %u, TVOC: %u, eCO2: %u, AQI: %u, Temp: %.1f°C, Hum: %.1f%%", pm_1_0,
|
||||
pm_2_5, pm_10_0, tvoc, eco2, aqi, temp_comp, hum_comp);
|
||||
|
||||
if (this->pm_1_0_std_sensor_ != nullptr)
|
||||
this->pm_1_0_std_sensor_->publish_state(u16(4));
|
||||
if (this->pm_2_5_std_sensor_ != nullptr)
|
||||
this->pm_2_5_std_sensor_->publish_state(u16(6));
|
||||
if (this->pm_10_0_std_sensor_ != nullptr)
|
||||
this->pm_10_0_std_sensor_->publish_state(u16(8));
|
||||
|
||||
if (this->pm_1_0_sensor_ != nullptr)
|
||||
this->pm_1_0_sensor_->publish_state(pm_1_0);
|
||||
if (this->pm_2_5_sensor_ != nullptr)
|
||||
this->pm_2_5_sensor_->publish_state(pm_2_5);
|
||||
if (this->pm_10_0_sensor_ != nullptr)
|
||||
this->pm_10_0_sensor_->publish_state(pm_10_0);
|
||||
|
||||
if (this->pm_0_3um_sensor_ != nullptr)
|
||||
this->pm_0_3um_sensor_->publish_state(u16(16));
|
||||
if (this->pm_0_5um_sensor_ != nullptr)
|
||||
this->pm_0_5um_sensor_->publish_state(u16(18));
|
||||
if (this->pm_1_0um_sensor_ != nullptr)
|
||||
this->pm_1_0um_sensor_->publish_state(u16(20));
|
||||
if (this->pm_2_5um_sensor_ != nullptr)
|
||||
this->pm_2_5um_sensor_->publish_state(u16(22));
|
||||
if (this->pm_5_0um_sensor_ != nullptr)
|
||||
this->pm_5_0um_sensor_->publish_state(u16(24));
|
||||
if (this->pm_10_0um_sensor_ != nullptr)
|
||||
this->pm_10_0um_sensor_->publish_state(u16(26));
|
||||
|
||||
if (this->tvoc_sensor_ != nullptr)
|
||||
this->tvoc_sensor_->publish_state(tvoc);
|
||||
if (this->eco2_sensor_ != nullptr)
|
||||
this->eco2_sensor_->publish_state(eco2);
|
||||
if (this->aqi_sensor_ != nullptr)
|
||||
this->aqi_sensor_->publish_state(aqi > 0 ? aqi : NAN);
|
||||
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temp_comp);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
this->humidity_sensor_->publish_state(hum_comp);
|
||||
|
||||
if (this->raw_temperature_sensor_ != nullptr) {
|
||||
this->raw_temperature_sensor_->publish_state(static_cast<int16_t>(u16(38)) / 10.0f);
|
||||
}
|
||||
if (this->raw_humidity_sensor_ != nullptr)
|
||||
this->raw_humidity_sensor_->publish_state(u16(40) / 10.0f);
|
||||
|
||||
if (this->rs0_sensor_ != nullptr) {
|
||||
this->rs0_sensor_->publish_state(u32(42));
|
||||
}
|
||||
if (this->rs2_sensor_ != nullptr) {
|
||||
this->rs2_sensor_->publish_state(u32(50));
|
||||
}
|
||||
if (this->rs3_sensor_ != nullptr) {
|
||||
this->rs3_sensor_->publish_state(u32(54));
|
||||
}
|
||||
}
|
||||
|
||||
void APC1Component::parse_device_info_frame_() {
|
||||
const uint8_t *serial = this->rx_buffer_.data() + APC1_DEVICE_INFO_OFFSET_SERIAL;
|
||||
uint16_t fw_version =
|
||||
encode_uint16(this->rx_buffer_[APC1_DEVICE_INFO_OFFSET_FW], this->rx_buffer_[APC1_DEVICE_INFO_OFFSET_FW + 1]);
|
||||
ESP_LOGI(TAG, "APC1: Module: %.6s, Serial: %08" PRIX32 "%08" PRIX32 ", Firmware: 0x%04X (%u)",
|
||||
reinterpret_cast<const char *>(this->rx_buffer_.data() + APC1_DEVICE_INFO_OFFSET_MODULE),
|
||||
encode_uint32(serial[0], serial[1], serial[2], serial[3]),
|
||||
encode_uint32(serial[4], serial[5], serial[6], serial[7]), fw_version, fw_version);
|
||||
}
|
||||
|
||||
void APC1Component::arm_watchdog_() {
|
||||
this->last_valid_frame_ = App.get_loop_component_start_time();
|
||||
this->last_init_attempt_ = this->last_valid_frame_;
|
||||
}
|
||||
|
||||
void APC1Component::set_active_mode() {
|
||||
if (!this->active_mode_) {
|
||||
this->arm_watchdog_();
|
||||
}
|
||||
this->active_mode_ = true;
|
||||
this->send_command_(APC1Command::APC1_COMMAND_MEASUREMENT_MODE, APC1_MEASUREMENT_MODE_ACTIVE);
|
||||
}
|
||||
|
||||
void APC1Component::set_passive_mode() {
|
||||
this->active_mode_ = false;
|
||||
this->send_command_(APC1Command::APC1_COMMAND_MEASUREMENT_MODE, APC1_MEASUREMENT_MODE_PASSIVE);
|
||||
}
|
||||
|
||||
void APC1Component::request_measurement() {
|
||||
this->send_command_(APC1Command::APC1_COMMAND_REQUEST_MEASUREMENT, 0x0000);
|
||||
}
|
||||
|
||||
void APC1Component::set_idle_mode() {
|
||||
this->idle_ = true;
|
||||
this->send_command_(APC1Command::APC1_COMMAND_OPERATION_MODE, APC1_OPERATING_MODE_IDLE);
|
||||
}
|
||||
|
||||
void APC1Component::set_measurement_mode() {
|
||||
this->idle_ = false;
|
||||
this->arm_watchdog_();
|
||||
this->send_command_(APC1Command::APC1_COMMAND_OPERATION_MODE, APC1_OPERATING_MODE_STANDARD);
|
||||
}
|
||||
|
||||
void APC1Component::send_command_(APC1Command cmd, uint16_t data) {
|
||||
std::array<uint8_t, APC1_COMMAND_FRAME_SIZE> buf{
|
||||
APC1_HEADER_1,
|
||||
APC1_HEADER_2,
|
||||
static_cast<uint8_t>(cmd),
|
||||
static_cast<uint8_t>((data >> 8) & 0xFF),
|
||||
static_cast<uint8_t>(data & 0xFF),
|
||||
0,
|
||||
0,
|
||||
};
|
||||
uint16_t checksum = apc1_checksum(buf.data(), 5);
|
||||
buf[5] = static_cast<uint8_t>((checksum >> 8) & 0xFF);
|
||||
buf[6] = static_cast<uint8_t>(checksum & 0xFF);
|
||||
this->write_array(buf);
|
||||
}
|
||||
|
||||
} // namespace esphome::apc1
|
||||
@@ -1,99 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome::apc1 {
|
||||
|
||||
static constexpr uint8_t APC1_HEADER_1 = 0x42;
|
||||
static constexpr uint8_t APC1_HEADER_2 = 0x4D;
|
||||
static constexpr uint8_t APC1_FRAME_HEADER_SIZE = 4; // 2 start bytes (0x42, 0x4D) + 2 frame length bytes
|
||||
static constexpr uint8_t APC1_FRAME_SIZE_MEASUREMENT = 64;
|
||||
static constexpr uint8_t APC1_FRAME_SIZE_DEVICE_INFO = 23;
|
||||
static constexpr uint8_t APC1_FRAME_SIZE_COMMAND_RESPONSE = 8;
|
||||
static constexpr uint8_t APC1_COMMAND_FRAME_SIZE = 7;
|
||||
|
||||
enum class APC1Command : uint8_t {
|
||||
APC1_COMMAND_MEASUREMENT_MODE = 0xE1,
|
||||
APC1_COMMAND_REQUEST_MEASUREMENT = 0xE2,
|
||||
APC1_COMMAND_OPERATION_MODE = 0xE4,
|
||||
APC1_COMMAND_READ_SENSOR_VERSION = 0xE9,
|
||||
};
|
||||
|
||||
static constexpr uint16_t APC1_MEASUREMENT_MODE_PASSIVE = 0x0000;
|
||||
static constexpr uint16_t APC1_MEASUREMENT_MODE_ACTIVE = 0x0001;
|
||||
|
||||
static constexpr uint16_t APC1_OPERATING_MODE_IDLE = 0x0000;
|
||||
static constexpr uint16_t APC1_OPERATING_MODE_STANDARD = 0x0001;
|
||||
|
||||
class APC1Component final : public uart::UARTDevice, public Component {
|
||||
SUB_SENSOR(pm_1_0)
|
||||
SUB_SENSOR(pm_2_5)
|
||||
SUB_SENSOR(pm_10_0)
|
||||
SUB_SENSOR(pm_1_0_std)
|
||||
SUB_SENSOR(pm_2_5_std)
|
||||
SUB_SENSOR(pm_10_0_std)
|
||||
|
||||
SUB_SENSOR(pm_0_3um)
|
||||
SUB_SENSOR(pm_0_5um)
|
||||
SUB_SENSOR(pm_1_0um)
|
||||
SUB_SENSOR(pm_2_5um)
|
||||
SUB_SENSOR(pm_5_0um)
|
||||
SUB_SENSOR(pm_10_0um)
|
||||
|
||||
SUB_SENSOR(tvoc)
|
||||
SUB_SENSOR(eco2)
|
||||
SUB_SENSOR(aqi)
|
||||
|
||||
SUB_SENSOR(temperature)
|
||||
SUB_SENSOR(humidity)
|
||||
SUB_SENSOR(raw_temperature)
|
||||
SUB_SENSOR(raw_humidity)
|
||||
|
||||
SUB_SENSOR(rs0)
|
||||
SUB_SENSOR(rs2)
|
||||
SUB_SENSOR(rs3)
|
||||
|
||||
SUB_SENSOR(error_code)
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void set_active_mode();
|
||||
void set_passive_mode();
|
||||
void request_measurement();
|
||||
void set_idle_mode();
|
||||
void set_measurement_mode();
|
||||
|
||||
void set_set_pin(GPIOPin *set_pin) { this->set_pin_ = set_pin; }
|
||||
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
|
||||
protected:
|
||||
void send_command_(APC1Command cmd, uint16_t data);
|
||||
void initialize_device_();
|
||||
/// Restarts the no-data watchdog so a mode change gets the same grace period as a cold boot
|
||||
void arm_watchdog_();
|
||||
void parse_frame_(uint16_t total_len);
|
||||
void parse_measurement_frame_();
|
||||
void parse_device_info_frame_();
|
||||
|
||||
GPIOPin *set_pin_{nullptr};
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
uint32_t last_transmission_{0};
|
||||
uint32_t last_valid_frame_{0};
|
||||
uint32_t last_init_attempt_{0};
|
||||
std::array<uint8_t, APC1_FRAME_SIZE_MEASUREMENT> rx_buffer_{};
|
||||
uint8_t rx_index_{0};
|
||||
uint8_t last_error_code_{0xFF};
|
||||
bool active_mode_{true};
|
||||
bool idle_{false};
|
||||
bool gas_warming_up_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::apc1
|
||||
@@ -1,222 +0,0 @@
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, uart
|
||||
from esphome.components.const import CONF_AQI
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ECO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_PM_0_3UM,
|
||||
CONF_PM_0_5UM,
|
||||
CONF_PM_1_0,
|
||||
CONF_PM_1_0_STD,
|
||||
CONF_PM_1_0UM,
|
||||
CONF_PM_2_5,
|
||||
CONF_PM_2_5_STD,
|
||||
CONF_PM_2_5UM,
|
||||
CONF_PM_5_0UM,
|
||||
CONF_PM_10_0,
|
||||
CONF_PM_10_0_STD,
|
||||
CONF_PM_10_0UM,
|
||||
CONF_RESET_PIN,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TVOC,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM1,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_CHEMICAL_WEAPON,
|
||||
ICON_MOLECULE_CO2,
|
||||
ICON_OMEGA,
|
||||
ICON_RADIATOR,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_COUNT_DECILITRE,
|
||||
UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
UNIT_OHM,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@ademuri"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
CONF_SET_PIN = "set_pin"
|
||||
CONF_RAW_TEMPERATURE = "raw_temperature"
|
||||
CONF_RAW_HUMIDITY = "raw_humidity"
|
||||
CONF_RS0 = "rs0"
|
||||
CONF_RS2 = "rs2"
|
||||
CONF_RS3 = "rs3"
|
||||
CONF_ERROR_CODE = "error_code"
|
||||
|
||||
apc1_ns = cg.esphome_ns.namespace("apc1")
|
||||
APC1Component = apc1_ns.class_("APC1Component", uart.UARTDevice, cg.Component)
|
||||
|
||||
|
||||
def _pm_mass_schema(device_class: str) -> cv.Schema:
|
||||
return sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=device_class,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
|
||||
_PM_COUNT_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNT_DECILITRE,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
_TEMPERATURE_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
_HUMIDITY_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
_RESISTANCE_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_OHM,
|
||||
icon=ICON_OMEGA,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(APC1Component),
|
||||
cv.Optional(CONF_PM_1_0): _pm_mass_schema(DEVICE_CLASS_PM1),
|
||||
cv.Optional(CONF_PM_2_5): _pm_mass_schema(DEVICE_CLASS_PM25),
|
||||
cv.Optional(CONF_PM_10_0): _pm_mass_schema(DEVICE_CLASS_PM10),
|
||||
cv.Optional(CONF_PM_1_0_STD): _pm_mass_schema(DEVICE_CLASS_PM1),
|
||||
cv.Optional(CONF_PM_2_5_STD): _pm_mass_schema(DEVICE_CLASS_PM25),
|
||||
cv.Optional(CONF_PM_10_0_STD): _pm_mass_schema(DEVICE_CLASS_PM10),
|
||||
cv.Optional(CONF_PM_0_3UM): _PM_COUNT_SCHEMA,
|
||||
cv.Optional(CONF_PM_0_5UM): _PM_COUNT_SCHEMA,
|
||||
cv.Optional(CONF_PM_1_0UM): _PM_COUNT_SCHEMA,
|
||||
cv.Optional(CONF_PM_2_5UM): _PM_COUNT_SCHEMA,
|
||||
cv.Optional(CONF_PM_5_0UM): _PM_COUNT_SCHEMA,
|
||||
cv.Optional(CONF_PM_10_0UM): _PM_COUNT_SCHEMA,
|
||||
cv.Optional(CONF_TVOC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ECO2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
icon=ICON_MOLECULE_CO2,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_AQI): sensor.sensor_schema(
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): _TEMPERATURE_SCHEMA,
|
||||
cv.Optional(CONF_HUMIDITY): _HUMIDITY_SCHEMA,
|
||||
cv.Optional(CONF_RAW_TEMPERATURE): _TEMPERATURE_SCHEMA,
|
||||
cv.Optional(CONF_RAW_HUMIDITY): _HUMIDITY_SCHEMA,
|
||||
cv.Optional(CONF_RS0): _RESISTANCE_SCHEMA,
|
||||
cv.Optional(CONF_RS2): _RESISTANCE_SCHEMA,
|
||||
cv.Optional(CONF_RS3): _RESISTANCE_SCHEMA,
|
||||
cv.Optional(CONF_ERROR_CODE): sensor.sensor_schema(
|
||||
icon="mdi:alert-circle-outline",
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_SET_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def final_validate(config: ConfigType) -> None:
|
||||
schema = uart.final_validate_device_schema(
|
||||
"apc1", baud_rate=9600, require_rx=True, require_tx=True
|
||||
)
|
||||
schema(config)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_PM_1_0, var.set_pm_1_0_sensor)
|
||||
await sensors(CONF_PM_2_5, var.set_pm_2_5_sensor)
|
||||
await sensors(CONF_PM_10_0, var.set_pm_10_0_sensor)
|
||||
await sensors(CONF_PM_1_0_STD, var.set_pm_1_0_std_sensor)
|
||||
await sensors(CONF_PM_2_5_STD, var.set_pm_2_5_std_sensor)
|
||||
await sensors(CONF_PM_10_0_STD, var.set_pm_10_0_std_sensor)
|
||||
await sensors(CONF_PM_0_3UM, var.set_pm_0_3um_sensor)
|
||||
await sensors(CONF_PM_0_5UM, var.set_pm_0_5um_sensor)
|
||||
await sensors(CONF_PM_1_0UM, var.set_pm_1_0um_sensor)
|
||||
await sensors(CONF_PM_2_5UM, var.set_pm_2_5um_sensor)
|
||||
await sensors(CONF_PM_5_0UM, var.set_pm_5_0um_sensor)
|
||||
await sensors(CONF_PM_10_0UM, var.set_pm_10_0um_sensor)
|
||||
await sensors(CONF_TVOC, var.set_tvoc_sensor)
|
||||
await sensors(CONF_ECO2, var.set_eco2_sensor)
|
||||
await sensors(CONF_AQI, var.set_aqi_sensor)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
|
||||
await sensors(CONF_RAW_TEMPERATURE, var.set_raw_temperature_sensor)
|
||||
await sensors(CONF_RAW_HUMIDITY, var.set_raw_humidity_sensor)
|
||||
await sensors(CONF_RS0, var.set_rs0_sensor)
|
||||
await sensors(CONF_RS2, var.set_rs2_sensor)
|
||||
await sensors(CONF_RS3, var.set_rs3_sensor)
|
||||
await sensors(CONF_ERROR_CODE, var.set_error_code_sensor)
|
||||
|
||||
if (set_pin_config := config.get(CONF_SET_PIN)) is not None:
|
||||
set_pin = await cg.gpio_pin_expression(set_pin_config)
|
||||
cg.add(var.set_set_pin(set_pin))
|
||||
|
||||
if (reset_pin_config := config.get(CONF_RESET_PIN)) is not None:
|
||||
reset_pin = await cg.gpio_pin_expression(reset_pin_config)
|
||||
cg.add(var.set_reset_pin(reset_pin))
|
||||
|
||||
|
||||
APC1_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(APC1Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
for _name, _call in (
|
||||
("apc1.set_active_mode", "set_active_mode()"),
|
||||
("apc1.set_passive_mode", "set_passive_mode()"),
|
||||
("apc1.request_measurement", "request_measurement()"),
|
||||
("apc1.set_idle_mode", "set_idle_mode()"),
|
||||
("apc1.set_measurement_mode", "set_measurement_mode()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, APC1_ACTION_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
@@ -1,2 +0,0 @@
|
||||
CODEOWNERS = ["@iyesin"]
|
||||
DOMAIN = "apds9930"
|
||||
@@ -1,140 +0,0 @@
|
||||
#include "apds9930.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::apds9930 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "apds9930");
|
||||
|
||||
// Register addresses
|
||||
static constexpr uint8_t APDS9930_ENABLE = 0x00;
|
||||
static constexpr uint8_t APDS9930_ATIME = 0x01;
|
||||
static constexpr uint8_t APDS9930_PTIME = 0x02;
|
||||
static constexpr uint8_t APDS9930_PPULSE = 0x0E;
|
||||
static constexpr uint8_t APDS9930_CONTROL = 0x0F;
|
||||
static constexpr uint8_t APDS9930_ID = 0x12;
|
||||
static constexpr uint8_t APDS9930_STATUS = 0x13;
|
||||
|
||||
// Command byte: repeated byte and auto-increment protocols
|
||||
static constexpr uint8_t APDS9930_CMD = 0x80;
|
||||
static constexpr uint8_t APDS9930_CMD_AUTO_INCREMENT = 0xA0;
|
||||
|
||||
static constexpr uint8_t APDS9930_CHIP_ID = 0x39;
|
||||
|
||||
// ENABLE register bits
|
||||
static constexpr uint8_t APDS9930_PON = 0x01;
|
||||
static constexpr uint8_t APDS9930_AEN = 0x02;
|
||||
static constexpr uint8_t APDS9930_PEN = 0x04;
|
||||
|
||||
// STATUS register bits
|
||||
static constexpr uint8_t APDS9930_AVALID = 0x01;
|
||||
static constexpr uint8_t APDS9930_PVALID = 0x02;
|
||||
|
||||
// CONTROL register fields
|
||||
static constexpr uint8_t CONTROL_PDRIVE_SHIFT = 6;
|
||||
static constexpr uint8_t CONTROL_PDIODE_SHIFT = 4;
|
||||
static constexpr uint8_t CONTROL_PGAIN_SHIFT = 2;
|
||||
static constexpr uint8_t CONTROL_AGAIN_SHIFT = 0;
|
||||
static constexpr uint8_t CONTROL_PDIODE_CH1 = 0b10; // The only supported proximity diode
|
||||
|
||||
static constexpr uint8_t APDS9930_ATIME_VALUE = 0xED; // 19 cycles, about 52 ms
|
||||
static constexpr uint8_t APDS9930_PTIME_VALUE = 0xFF; // 2.73 ms
|
||||
static constexpr uint8_t APDS9930_PPULSE_VALUE = 0x08; // 8 pulses
|
||||
|
||||
// Lux calculation coefficients
|
||||
static constexpr float APDS9930_DF = 52.0f;
|
||||
static constexpr float APDS9930_GA = 0.49f;
|
||||
static constexpr float APDS9930_ALS_B = 1.862f;
|
||||
static constexpr float APDS9930_ALS_C = 0.746f;
|
||||
static constexpr float APDS9930_ALS_D = 1.291f;
|
||||
static constexpr float APDS9930_ALSIT_MS = 2.73f * (256 - APDS9930_ATIME_VALUE);
|
||||
|
||||
static constexpr uint8_t AMBIENT_GAINS[4] PROGMEM = {1, 8, 16, 120};
|
||||
static constexpr uint8_t PROXIMITY_GAINS[4] PROGMEM = {1, 2, 4, 8};
|
||||
static constexpr float LED_CURRENT_MAX_MA = 100.0f; // halved for each drive step
|
||||
|
||||
// Lux per count at 1x gain and the fixed integration time
|
||||
static constexpr float LUX_PER_COUNT_1X = APDS9930_GA * APDS9930_DF / APDS9930_ALSIT_MS;
|
||||
// Each integration cycle adds up to 1024 counts, so this is the ceiling of both light channels
|
||||
static constexpr uint16_t ALS_SATURATION = 1024 * (256 - APDS9930_ATIME_VALUE);
|
||||
|
||||
bool APDS9930Component::write_reg_(uint8_t reg, uint8_t value) { return this->write_byte(reg | APDS9930_CMD, value); }
|
||||
|
||||
void APDS9930Component::setup() {
|
||||
uint8_t id;
|
||||
if (!this->read_byte(APDS9930_ID | APDS9930_CMD, &id)) {
|
||||
this->mark_failed(LOG_STR("Communication failed"));
|
||||
return;
|
||||
}
|
||||
if (id != APDS9930_CHIP_ID) {
|
||||
ESP_LOGE(TAG, "Wrong ID 0x%02X (expected 0x%02X)", id, APDS9930_CHIP_ID);
|
||||
this->mark_failed(LOG_STR("Wrong chip ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t control =
|
||||
(this->led_drive_ & 0b11) << CONTROL_PDRIVE_SHIFT | CONTROL_PDIODE_CH1 << CONTROL_PDIODE_SHIFT |
|
||||
(this->proximity_gain_ & 0b11) << CONTROL_PGAIN_SHIFT | (this->ambient_gain_ & 0b11) << CONTROL_AGAIN_SHIFT;
|
||||
|
||||
uint8_t enable = APDS9930_PON;
|
||||
if (this->illuminance_sensor_ != nullptr)
|
||||
enable |= APDS9930_AEN;
|
||||
if (this->proximity_sensor_ != nullptr)
|
||||
enable |= APDS9930_PEN;
|
||||
|
||||
if (!this->write_reg_(APDS9930_ENABLE, 0x00) || !this->write_reg_(APDS9930_ATIME, APDS9930_ATIME_VALUE) ||
|
||||
!this->write_reg_(APDS9930_PTIME, APDS9930_PTIME_VALUE) ||
|
||||
!this->write_reg_(APDS9930_PPULSE, APDS9930_PPULSE_VALUE) || !this->write_reg_(APDS9930_CONTROL, control) ||
|
||||
!this->write_reg_(APDS9930_ENABLE, enable)) {
|
||||
this->mark_failed(LOG_STR("Configuration failed"));
|
||||
}
|
||||
}
|
||||
|
||||
void APDS9930Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"APDS9930:\n"
|
||||
" LED Drive: %.1f mA\n"
|
||||
" Proximity Gain: %ux\n"
|
||||
" Ambient Light Gain: %ux",
|
||||
LED_CURRENT_MAX_MA / (1 << this->led_drive_),
|
||||
progmem_read_byte(&PROXIMITY_GAINS[this->proximity_gain_]),
|
||||
progmem_read_byte(&AMBIENT_GAINS[this->ambient_gain_]));
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
|
||||
LOG_SENSOR(" ", "Proximity", this->proximity_sensor_);
|
||||
}
|
||||
|
||||
void APDS9930Component::update() {
|
||||
// STATUS, CH0DATAL/H, CH1DATAL/H, PDATAL/H
|
||||
uint8_t raw[7];
|
||||
if (!this->read_bytes(APDS9930_STATUS | APDS9930_CMD_AUTO_INCREMENT, raw, sizeof(raw))) {
|
||||
this->status_set_warning(LOG_STR("Reading data failed"));
|
||||
return;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
const uint8_t status = raw[0];
|
||||
|
||||
if (this->illuminance_sensor_ != nullptr && (status & APDS9930_AVALID) != 0) {
|
||||
const uint16_t ch0 = encode_uint16(raw[2], raw[1]);
|
||||
const uint16_t ch1 = encode_uint16(raw[4], raw[3]);
|
||||
if (ch0 >= ALS_SATURATION || ch1 >= ALS_SATURATION) {
|
||||
// A saturated channel would give a lux value that is too low, not too high
|
||||
ESP_LOGW(TAG, "Light channel saturated, lower ambient_light_gain");
|
||||
this->illuminance_sensor_->publish_state(NAN);
|
||||
} else {
|
||||
const float iac = std::max({ch0 - APDS9930_ALS_B * ch1, APDS9930_ALS_C * ch0 - APDS9930_ALS_D * ch1, 0.0f});
|
||||
this->illuminance_sensor_->publish_state(iac * LUX_PER_COUNT_1X /
|
||||
progmem_read_byte(&AMBIENT_GAINS[this->ambient_gain_]));
|
||||
}
|
||||
}
|
||||
|
||||
if (this->proximity_sensor_ != nullptr && (status & APDS9930_PVALID) != 0) {
|
||||
this->proximity_sensor_->publish_state(encode_uint16(raw[6], raw[5]));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::apds9930
|
||||
@@ -1,30 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::apds9930 {
|
||||
|
||||
class APDS9930Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
SUB_SENSOR(illuminance)
|
||||
SUB_SENSOR(proximity)
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void set_led_drive(uint8_t level) { this->led_drive_ = level; }
|
||||
void set_proximity_gain(uint8_t gain) { this->proximity_gain_ = gain; }
|
||||
void set_ambient_gain(uint8_t gain) { this->ambient_gain_ = gain; }
|
||||
|
||||
protected:
|
||||
bool write_reg_(uint8_t reg, uint8_t value);
|
||||
|
||||
uint8_t led_drive_{0};
|
||||
uint8_t proximity_gain_{0};
|
||||
uint8_t ambient_gain_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::apds9930
|
||||
@@ -1,70 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_ILLUMINANCE,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_LUX,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
CONF_AMBIENT_LIGHT_GAIN = "ambient_light_gain"
|
||||
CONF_LED_DRIVE = "led_drive"
|
||||
CONF_PROXIMITY = "proximity"
|
||||
CONF_PROXIMITY_GAIN = "proximity_gain"
|
||||
|
||||
DRIVE_LEVELS = {"100ma": 0, "50ma": 1, "25ma": 2, "12.5ma": 3}
|
||||
PROXIMITY_LEVELS = {"1x": 0, "2x": 1, "4x": 2, "8x": 3}
|
||||
AMBIENT_LEVELS = {"1x": 0, "8x": 1, "16x": 2, "120x": 3}
|
||||
|
||||
apds9930_ns = cg.esphome_ns.namespace("apds9930")
|
||||
APDS9930Component = apds9930_ns.class_(
|
||||
"APDS9930Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(APDS9930Component),
|
||||
cv.Optional(CONF_LED_DRIVE, default="100ma"): cv.enum(
|
||||
DRIVE_LEVELS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_PROXIMITY_GAIN, default="8x"): cv.enum(
|
||||
PROXIMITY_LEVELS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_AMBIENT_LIGHT_GAIN, default="1x"): cv.enum(
|
||||
AMBIENT_LEVELS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_LUX,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PROXIMITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNTS,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x39))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_led_drive(config[CONF_LED_DRIVE]))
|
||||
cg.add(var.set_proximity_gain(config[CONF_PROXIMITY_GAIN]))
|
||||
cg.add(var.set_ambient_gain(config[CONF_AMBIENT_LIGHT_GAIN]))
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_ILLUMINANCE, var.set_illuminance_sensor)
|
||||
await sensors(CONF_PROXIMITY, var.set_proximity_sensor)
|
||||
@@ -54,7 +54,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
from esphome.cpp_generator import Expression, MockObj, TemplateArgsType
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import fnv1_hash
|
||||
from esphome.helpers import cpp_string_escape, fnv1_hash
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigFragmentType, ConfigType
|
||||
|
||||
@@ -537,11 +537,28 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _add_action_strings(index: int, strings: list[str | None]) -> MockObj:
|
||||
"""Emit the PROGMEM string table for one action."""
|
||||
def _add_action_strings(
|
||||
index: int, strings: list[str | None], interned: dict[str, MockObj]
|
||||
) -> MockObj:
|
||||
"""Emit the PROGMEM string table for one action.
|
||||
|
||||
Each string is its own PROGMEM array because on ESP8266 .rodata is RAM, and identical
|
||||
strings are shared between actions through `interned`.
|
||||
"""
|
||||
entries: list[MockObj] = []
|
||||
for string in strings:
|
||||
if string is None:
|
||||
entries.append(cg.nullptr)
|
||||
continue
|
||||
if (var := interned.get(string)) is None:
|
||||
var = interned[string] = cg.progmem_array(
|
||||
ID(f"api_action_str{len(interned)}", is_declaration=True, type=cg.char),
|
||||
string,
|
||||
)
|
||||
entries.append(var)
|
||||
return cg.progmem_array(
|
||||
ID(f"api_action{index}_strings", is_declaration=True, type=cg.const_char_ptr),
|
||||
[cg.nullptr if s is None else cg.shared_progmem_string(s) for s in strings],
|
||||
entries,
|
||||
)
|
||||
|
||||
|
||||
@@ -594,6 +611,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
has_metadata = _has_action_metadata(actions)
|
||||
if has_metadata:
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTION_METADATA")
|
||||
interned_strings: dict[str, MockObj] = {}
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.MockObj] = []
|
||||
for index, conf in enumerate(actions):
|
||||
@@ -638,7 +656,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
service_template_args.append(native)
|
||||
func_args.append((native, name))
|
||||
strings = _action_strings(conf, has_metadata)
|
||||
table = _add_action_strings(index, strings)
|
||||
table = _add_action_strings(index, strings, interned_strings)
|
||||
if CORE.is_esp8266:
|
||||
scratch_size = max(scratch_size, _action_strings_size(strings))
|
||||
# Template args: supports_response mode, then user service arg types
|
||||
@@ -664,7 +682,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
unregister_id,
|
||||
var,
|
||||
)
|
||||
cg.add(auto.add_action(unregister_action))
|
||||
cg.add(auto.add_actions([unregister_action]))
|
||||
# Register all services at once - single allocation, no reallocations
|
||||
cg.add(var.initialize_user_services(triggers))
|
||||
if CORE.is_esp8266 and has_user_actions:
|
||||
@@ -810,6 +828,14 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _field_string(value: str) -> Expression:
|
||||
# ESP8266 can only keep a string in flash as its own PROGMEM array
|
||||
literal = cg.RawExpression(cpp_string_escape(value))
|
||||
if CORE.is_esp8266:
|
||||
return cg.shared_progmem_array("ha_field_str", cg.char, literal)
|
||||
return literal
|
||||
|
||||
|
||||
async def _new_service_call_action(
|
||||
server_id: ID,
|
||||
action_id: ID,
|
||||
@@ -832,12 +858,12 @@ async def _new_service_call_action(
|
||||
)
|
||||
|
||||
async def field(key: str | None, value: Any, output_type: Any = None) -> Expression:
|
||||
key_exp = cg.nullptr if key is None else cg.shared_progmem_string(key)
|
||||
key_exp = cg.nullptr if key is None else _field_string(key)
|
||||
if cg.is_template(value):
|
||||
# output_type=None lets lambdas return numbers or char pointers; C++ converts them
|
||||
lam = await cg.process_lambda(value, args, return_type=output_type)
|
||||
return cg.RawExpression(f"{field_type}::from_lambda({key_exp}, {lam})")
|
||||
return cg.ArrayInitializer(key_exp, cg.shared_progmem_string(value), cg.nullptr)
|
||||
return cg.ArrayInitializer(key_exp, _field_string(value), cg.nullptr)
|
||||
|
||||
entries = [await field(None, service, cg.std_string)]
|
||||
for group in groups:
|
||||
|
||||
@@ -444,7 +444,7 @@ message DeviceWizardRequest {
|
||||
// {"version": 1,
|
||||
// "pages": [{"title": "...", "description": "...",
|
||||
// "entities": [{"key": 123, "device_id": 456, "description": "..."}],
|
||||
// "inputs": [{"key": 789, "description": "...", "multiple": true, "max_length": 1023,
|
||||
// "inputs": [{"key": 789, "description": "...",
|
||||
// "entity_filters": [{"integration": "...", "domain": ["..."],
|
||||
// "device_class": ["..."], "supported_features": ["..."]}]}]}]}
|
||||
//
|
||||
@@ -468,14 +468,6 @@ message DeviceWizardRequest {
|
||||
// an entity matches a filter when it satisfies every field that is set, and an entity
|
||||
// that matches any one of the filters is accepted.
|
||||
//
|
||||
// An input with "multiple": true lets the user pick several entities. The client sends
|
||||
// them in one WizardInputSetRequest as a comma separated list with no spaces
|
||||
// ("calendar.a,calendar.b"); the device also accepts spaces around each id and stores the
|
||||
// list without them. "max_length" (only on a multiple input, 255 when left out) is the
|
||||
// longest list in bytes the device accepts. The version stays 1: a client that does not
|
||||
// know "multiple" ignores it, shows a single picker and sends one entity id, which a
|
||||
// multiple input accepts.
|
||||
//
|
||||
// The device keeps the entity id of an input in RAM only and does not store it across
|
||||
// restarts. The client therefore stores the choice itself and, after every connection
|
||||
// and before it sends SubscribeHomeAssistantStatesRequest, sends a WizardInputSetRequest
|
||||
@@ -491,13 +483,8 @@ message DeviceWizardResponse {
|
||||
|
||||
// Sets the entity id of a wizard input (see WizardInputField.key). Clients send
|
||||
// this only when DeviceCapabilitiesResponse.wizard.configured is set. There is no
|
||||
// reply. The device ignores a request whose key matches no input. A single input is
|
||||
// ignored when the entity_id is empty, longer than 255 bytes or has no '.'. A multiple
|
||||
// input takes a comma separated list ("calendar.a,calendar.b"); the client sends no
|
||||
// spaces, the device trims spaces around each id and stores the list without them. The
|
||||
// whole request is ignored when any id is empty or has no '.', or when the list without
|
||||
// spaces does not fit the input's max_length (255 by default). Otherwise it keeps the id
|
||||
// or list in RAM,
|
||||
// reply. The device ignores a request whose key matches no input or whose entity_id
|
||||
// is empty, longer than 255 bytes or has no '.'. Otherwise it keeps the id in RAM,
|
||||
// not across restarts, and when the input belongs to a homeassistant platform entity
|
||||
// it subscribes to that entity's state. Clients send this for every input right
|
||||
// after connecting, before SubscribeHomeAssistantStatesRequest. A request that
|
||||
@@ -509,7 +496,7 @@ message WizardInputSetRequest {
|
||||
option (ifdef) = "USE_API_WIZARD_INPUTS";
|
||||
|
||||
fixed32 key = 1;
|
||||
string entity_id = 2 [(max_data_length) = 4095];
|
||||
string entity_id = 2 [(max_data_length) = 255];
|
||||
}
|
||||
|
||||
message ListEntitiesRequest {
|
||||
@@ -2570,7 +2557,7 @@ message ListEntitiesEventResponse {
|
||||
EntityCategory entity_category = 7;
|
||||
string device_class = 8 [(max_data_length) = 47];
|
||||
|
||||
repeated string event_types = 9 [(container_pointer_no_template) = "ConstVector<const char *, true>"];
|
||||
repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
message EventResponse {
|
||||
|
||||
@@ -3460,7 +3460,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_event_response"); }
|
||||
#endif
|
||||
StringRef device_class{nullptr, 0}; // null until set, encode only
|
||||
const ConstVector<const char *, true> *event_types{};
|
||||
const FixedVector<const char *> *event_types{};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -237,8 +237,6 @@ void APIServer::refill_spare_ephemeral_() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Generating the key pair cannot be split across loop passes
|
||||
UnavoidableBlockingScope scope;
|
||||
noise::prepare_spare_ephemeral();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -14,68 +14,26 @@ namespace esphome::api {
|
||||
ESPHOME_LOG_TAG(TAG, "api.wizard");
|
||||
|
||||
#ifdef USE_API_WIZARD_INPUTS
|
||||
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
// Walks the comma separated entity ids in value, without the spaces around each. Returns the length they take joined
|
||||
// by ',', or 0 when one is empty or has no '.'. Writes the joined list to out unless it is nullptr.
|
||||
static size_t wizard_join_entity_ids(const char *value, size_t length, char *out) {
|
||||
size_t joined = 0;
|
||||
const char *end = value + length;
|
||||
while (true) {
|
||||
const char *comma = static_cast<const char *>(memchr(value, ',', end - value));
|
||||
const char *id_end = comma != nullptr ? comma : end;
|
||||
while (value < id_end && *value == ' ')
|
||||
value++;
|
||||
while (id_end > value && id_end[-1] == ' ')
|
||||
id_end--;
|
||||
size_t id_length = id_end - value;
|
||||
if (id_length == 0 || memchr(value, '.', id_length) == nullptr)
|
||||
return 0;
|
||||
if (joined != 0) {
|
||||
if (out != nullptr)
|
||||
out[joined] = ',';
|
||||
joined++;
|
||||
}
|
||||
if (out != nullptr)
|
||||
memcpy(out + joined, value, id_length);
|
||||
joined += id_length;
|
||||
if (comma == nullptr)
|
||||
return joined;
|
||||
value = comma + 1;
|
||||
}
|
||||
}
|
||||
#endif // USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
|
||||
static bool wizard_entity_id_valid(const char *entity_id, size_t length) {
|
||||
return length > 0 && length < WIZARD_ENTITY_ID_BUFFER_SIZE && memchr(entity_id, '.', length) != nullptr;
|
||||
}
|
||||
|
||||
const char *wizard_set_input(const WizardInputSetRequest &msg) {
|
||||
const char *value = msg.entity_id.c_str();
|
||||
const size_t length = msg.entity_id.size();
|
||||
if (!wizard_entity_id_valid(msg.entity_id.c_str(), msg.entity_id.size())) {
|
||||
ESP_LOGW(TAG, "Ignoring an invalid entity id for wizard input");
|
||||
return nullptr;
|
||||
}
|
||||
for (size_t i = 0; i < API_WIZARD_INPUT_COUNT; i++) {
|
||||
// The table is in flash, which ESP8266 can only read through progmem_memcpy
|
||||
WizardInputEntry entry;
|
||||
progmem_memcpy(&entry, &API_WIZARD_INPUTS[i], sizeof(entry));
|
||||
if (entry.key != msg.key)
|
||||
continue;
|
||||
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
if (entry.size != 0) {
|
||||
// Two passes, validate and measure then write, so a rejected request leaves the buffer unchanged
|
||||
const size_t joined = wizard_join_entity_ids(value, length, nullptr);
|
||||
if (joined == 0 || joined >= entry.size)
|
||||
break;
|
||||
wizard_join_entity_ids(value, length, entry.entity_id);
|
||||
entry.entity_id[joined] = '\0';
|
||||
return entry.entity_id;
|
||||
}
|
||||
#endif
|
||||
if (!wizard_entity_id_valid(value, length))
|
||||
break;
|
||||
memcpy(entry.entity_id, value, length);
|
||||
entry.entity_id[length] = '\0';
|
||||
memcpy(entry.entity_id, msg.entity_id.c_str(), msg.entity_id.size());
|
||||
entry.entity_id[msg.entity_id.size()] = '\0';
|
||||
return entry.entity_id;
|
||||
}
|
||||
ESP_LOGW(TAG, "Ignoring an entity id for an unknown wizard input, or one that is not valid");
|
||||
ESP_LOGW(TAG, "Ignoring an entity id for an unknown wizard input");
|
||||
return nullptr;
|
||||
}
|
||||
#endif // USE_API_WIZARD_INPUTS
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "proto.h"
|
||||
@@ -29,10 +26,7 @@ extern const uint8_t API_WIZARD_DATA[] PROGMEM;
|
||||
/// Where the entity id of an input is kept, found by the key the client uses for it.
|
||||
struct WizardInputEntry {
|
||||
uint32_t key; // FNV-1 hash of the ESPHome id of the input
|
||||
char *entity_id; // RAM buffer, WIZARD_ENTITY_ID_BUFFER_SIZE bytes or size, shared with the homeassistant entity
|
||||
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
uint16_t size; // Buffer size of a multiple input, 0 for a single input
|
||||
#endif
|
||||
char *entity_id; // RAM buffer of WIZARD_ENTITY_ID_BUFFER_SIZE bytes, shared with the homeassistant entity
|
||||
};
|
||||
|
||||
/// The inputs of the wizard, in flash. API_WIZARD_INPUT_COUNT entries long.
|
||||
@@ -50,34 +44,9 @@ const char *wizard_set_input(const WizardInputSetRequest &msg);
|
||||
class WizardInput {
|
||||
public:
|
||||
explicit WizardInput(const char *entity_id) : entity_id_(entity_id) {}
|
||||
/// The Home Assistant entity ID, empty until the wizard sets one. For a multiple input this is the whole
|
||||
/// comma separated list, which homeassistant.action data can use as it is.
|
||||
/// The Home Assistant entity ID, empty until the wizard sets one.
|
||||
StringRef entity_id() const { return StringRef(this->entity_id_); }
|
||||
bool has_entity_id() const { return this->entity_id_[0] != '\0'; }
|
||||
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
|
||||
/// The number of entity IDs in the list, 0 until the wizard sets one.
|
||||
size_t entity_id_count() const {
|
||||
if (!this->has_entity_id())
|
||||
return 0;
|
||||
size_t count = 1;
|
||||
for (const char *p = this->entity_id_; (p = strchr(p, ',')) != nullptr; p++)
|
||||
count++;
|
||||
return count;
|
||||
}
|
||||
/// Call callback(StringRef) for each entity ID in the list, in order. The StringRef points into the list and is
|
||||
/// not null terminated, so print it with "%.*s".
|
||||
template<typename F> void for_each_entity_id(F &&callback) const {
|
||||
// The device stores the list without spaces or empty entries
|
||||
const char *start = this->entity_id_;
|
||||
while (*start != '\0') {
|
||||
const char *end = strchr(start, ',');
|
||||
if (end == nullptr)
|
||||
end = start + strlen(start);
|
||||
callback(StringRef(start, end - start));
|
||||
start = *end == ',' ? end + 1 : end;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
const char *entity_id_;
|
||||
|
||||
@@ -20,8 +20,6 @@ from esphome.const import (
|
||||
CONF_ENTITY_ID,
|
||||
CONF_ID,
|
||||
CONF_INTERNAL,
|
||||
CONF_MAX_LENGTH,
|
||||
CONF_MULTIPLE,
|
||||
CONF_NAME,
|
||||
CONF_PAGES,
|
||||
CONF_PLATFORM,
|
||||
@@ -53,10 +51,6 @@ WizardInput = _API.class_("WizardInput")
|
||||
WIZARD_ENTITY_ID_BUFFER_SIZE = (
|
||||
256 # api_wizard.h; Home Assistant entity IDs are at most 255 bytes
|
||||
)
|
||||
WIZARD_ENTITY_ID_MAX_LENGTH = WIZARD_ENTITY_ID_BUFFER_SIZE - 1
|
||||
# The value of a multiple input fits one WizardInputSetRequest, which the device accepts up to 8 KiB on ESP8266;
|
||||
# api.proto max_data_length documents the same value
|
||||
WIZARD_MULTIPLE_MAX_LENGTH = 4095
|
||||
# One API message must fit APIBuffer::MAX_SIZE (65535) together with the largest frame header (7 bytes,
|
||||
# Noise) and footer (16 bytes, Noise MAC)
|
||||
WIZARD_RESPONSE_MAX_SIZE = 65535 - 7 - 16
|
||||
@@ -112,21 +106,6 @@ WIZARD_ENTITY_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _validate_multiple(conf: ConfigType) -> ConfigType:
|
||||
if conf[CONF_MULTIPLE] and CONF_ENTITY in conf:
|
||||
raise cv.Invalid(
|
||||
"A linked input follows the state of exactly one Home Assistant entity, so multiple "
|
||||
"is only for standalone inputs (id)",
|
||||
path=[CONF_MULTIPLE],
|
||||
)
|
||||
if CONF_MAX_LENGTH in conf and not conf[CONF_MULTIPLE]:
|
||||
raise cv.Invalid(
|
||||
"max_length only applies to an input with multiple: true",
|
||||
path=[CONF_MAX_LENGTH],
|
||||
)
|
||||
return conf
|
||||
|
||||
|
||||
# An input is either standalone (id declares a new WizardInput) or linked to a homeassistant entity that has no
|
||||
# entity_id of its own: Home Assistant sets it
|
||||
WIZARD_INPUT_SCHEMA = cv.All(
|
||||
@@ -135,10 +114,6 @@ WIZARD_INPUT_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ID): cv.declare_id(WizardInput),
|
||||
cv.Optional(CONF_ENTITY): cv.use_id(cg.EntityBase),
|
||||
cv.Optional(CONF_DESCRIPTION): _wizard_text(WIZARD_DESCRIPTION_MAX_LENGTH),
|
||||
cv.Optional(CONF_MULTIPLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MAX_LENGTH): cv.int_range(
|
||||
min=WIZARD_ENTITY_ID_MAX_LENGTH, max=WIZARD_MULTIPLE_MAX_LENGTH
|
||||
),
|
||||
cv.Optional(CONF_TARGET): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ENTITY): cv.All(
|
||||
@@ -149,7 +124,6 @@ WIZARD_INPUT_SCHEMA = cv.All(
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_ID, CONF_ENTITY),
|
||||
_validate_multiple,
|
||||
)
|
||||
|
||||
WIZARD_PAGE_SCHEMA = cv.All(
|
||||
@@ -274,8 +248,6 @@ def _wizard_defines(wizard: ConfigType) -> set[str]:
|
||||
defines.add("USE_API_WIZARD_LINKED_INPUTS")
|
||||
if any(CONF_ID in conf for conf in inputs):
|
||||
defines.add("USE_API_WIZARD_STANDALONE_INPUTS")
|
||||
if any(conf.get(CONF_MULTIPLE) for conf in inputs):
|
||||
defines.add("USE_API_WIZARD_MULTIPLE_INPUTS")
|
||||
return defines
|
||||
|
||||
|
||||
@@ -399,10 +371,6 @@ def _input_document(conf: ConfigType, config: fv.FinalValidateConfig) -> ConfigT
|
||||
document: ConfigType = {"key": fnv1_hash(_wizard_input_id(conf).id)}
|
||||
if description := conf.get(CONF_DESCRIPTION):
|
||||
document[CONF_DESCRIPTION] = description
|
||||
if conf.get(CONF_MULTIPLE):
|
||||
document["multiple"] = True
|
||||
if (max_length := conf.get(CONF_MAX_LENGTH)) is not None:
|
||||
document["max_length"] = max_length
|
||||
if filters := _wizard_input_filters(conf, config):
|
||||
document["entity_filters"] = [dict(entity_filter) for entity_filter in filters]
|
||||
return document
|
||||
@@ -416,7 +384,7 @@ def wizard_document(wizard: ConfigType, config: fv.FinalValidateConfig) -> Confi
|
||||
{"version": 1,
|
||||
"pages": [{"title": "...", "description": "...",
|
||||
"entities": [{"key": 123, "device_id": 456, "description": "..."}],
|
||||
"inputs": [{"key": 789, "description": "...", "multiple": true, "max_length": 1023,
|
||||
"inputs": [{"key": 789, "description": "...",
|
||||
"entity_filters": [{"integration": "...", "domain": ["..."],
|
||||
"device_class": ["..."], "supported_features": ["..."]}]}]}]}
|
||||
|
||||
@@ -428,14 +396,6 @@ def wizard_document(wizard: ConfigType, config: fv.FinalValidateConfig) -> Confi
|
||||
the device it belongs to (esphome/core/config.py), and is left out for the main device.
|
||||
- An input key is the FNV-1 hash of the ESPHome id of the input, or of the linked entity. A linked
|
||||
entity must not set entity_id, as that is a static entry kept apart from the ones the wizard sets.
|
||||
- multiple means the user may pick several entities. The client sends them in one
|
||||
WizardInputSetRequest as a comma separated list ("calendar.a,calendar.b") with no spaces; the
|
||||
device trims spaces around each id and stores the list without them. The whole request is
|
||||
ignored when any id is empty or has no '.', or when the list without spaces does not fit
|
||||
max_length (only on a multiple input, 255 when left out). A single input is ignored when the id
|
||||
is empty, longer than 255 bytes or has no '.'. The version stays 1: a client that does not know
|
||||
multiple ignores it, shows a single picker and sends one entity id, which a multiple input
|
||||
accepts.
|
||||
- entity_filters are the filters of the input, or the default filters of a linked switch,
|
||||
number, text, select or button.
|
||||
"""
|
||||
@@ -481,15 +441,14 @@ async def to_code(wizard: ConfigType) -> None:
|
||||
input_id = _wizard_input_id(conf)
|
||||
# Every buffer starts empty, until the wizard sets it. A linked homeassistant entity uses it as its entity id.
|
||||
buffer = _wizard_buffer_name(input_id)
|
||||
size = conf.get(CONF_MAX_LENGTH, WIZARD_ENTITY_ID_MAX_LENGTH) + 1
|
||||
cg.add_global(cg.RawStatement(f'static char {buffer}[{size}] = "";'))
|
||||
cg.add_global(
|
||||
cg.RawStatement(
|
||||
f'static char {buffer}[{WIZARD_ENTITY_ID_BUFFER_SIZE}] = "";'
|
||||
)
|
||||
)
|
||||
if CONF_ID in conf:
|
||||
cg.new_Pvariable(input_id, cg.RawExpression(buffer))
|
||||
key = fnv1_hash(input_id.id)
|
||||
if conf[CONF_MULTIPLE]:
|
||||
entries.append(cg.RawExpression(f"{{{key}u, {buffer}, {size}}}"))
|
||||
else:
|
||||
entries.append(cg.RawExpression(f"{{{key}u, {buffer}}}"))
|
||||
entries.append(cg.RawExpression(f"{{{fnv1_hash(input_id.id)}u, {buffer}}}"))
|
||||
if entries:
|
||||
cg.extern_progmem_array(
|
||||
"esphome::api::API_WIZARD_INPUTS",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_AQI # noqa: F401
|
||||
|
||||
CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"]
|
||||
DOMAIN = "aqi"
|
||||
@@ -7,6 +6,7 @@ DOMAIN = "aqi"
|
||||
aqi_ns = cg.esphome_ns.namespace("aqi")
|
||||
AQICalculatorType = aqi_ns.enum("AQICalculatorType")
|
||||
|
||||
CONF_AQI = "aqi"
|
||||
CONF_CALCULATION_TYPE = "calculation_type"
|
||||
CONF_EXTENDED_RANGE = "extended_range"
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
CODEOWNERS = ["@patrickod"]
|
||||
DOMAIN = "aranet"
|
||||
@@ -1,157 +0,0 @@
|
||||
#include "aranet.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::aranet {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "aranet");
|
||||
static constexpr uint16_t ARANET_MANUFACTURER_ID = 0x0702;
|
||||
static constexpr size_t ARANET4_DATA_LENGTH = 22;
|
||||
static constexpr size_t OTHER_ARANET_DATA_LENGTH = 24;
|
||||
|
||||
enum class AranetDeviceType : uint8_t {
|
||||
ARANET_DEVICE_TYPE_ARANET4 = 0,
|
||||
ARANET_DEVICE_TYPE_ARANET2 = 1,
|
||||
ARANET_DEVICE_TYPE_RADIATION = 2,
|
||||
ARANET_DEVICE_TYPE_RADON = 3,
|
||||
};
|
||||
|
||||
static inline void publish_if_set(sensor::Sensor *sensor, float value) {
|
||||
if (sensor != nullptr)
|
||||
sensor->publish_state(value);
|
||||
}
|
||||
|
||||
// Aranet does not publish an official advertisement specification. The layout below is corroborated by these
|
||||
// independently maintained implementations:
|
||||
// https://github.com/Anrijs/Aranet4-Python/blob/master/aranet4/client.py
|
||||
// https://github.com/Anrijs/Aranet4-ESP32/blob/main/src/Aranet4.h
|
||||
|
||||
void Aranet::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Aranet");
|
||||
LOG_SENSOR(" ", "Carbon Dioxide", this->co2_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_sensor_);
|
||||
LOG_SENSOR(" ", "Measurement Interval", this->measurement_interval_sensor_);
|
||||
LOG_SENSOR(" ", "Measurement Age", this->measurement_age_sensor_);
|
||||
LOG_SENSOR(" ", "Signal Strength", this->signal_strength_sensor_);
|
||||
LOG_SENSOR(" ", "Radon Concentration", this->radon_sensor_);
|
||||
LOG_SENSOR(" ", "Radiation Dose Rate", this->radiation_rate_sensor_);
|
||||
LOG_SENSOR(" ", "Radiation Total Dose", this->radiation_total_sensor_);
|
||||
LOG_SENSOR(" ", "Radiation Dose Duration", this->radiation_duration_sensor_);
|
||||
}
|
||||
|
||||
bool Aranet::parse_device(const ble_device_base::ESPBTDevice &device) {
|
||||
if (device.address_uint64() != this->address_)
|
||||
return false;
|
||||
|
||||
const auto manufacturer_uuid = ble_device_base::ESPBTUUID::from_uint16(ARANET_MANUFACTURER_ID);
|
||||
for (const auto &manufacturer_data : device.get_manufacturer_datas()) {
|
||||
if (manufacturer_data.uuid != manufacturer_uuid)
|
||||
continue;
|
||||
|
||||
const auto &data = manufacturer_data.data;
|
||||
if (data.size() != ARANET4_DATA_LENGTH && data.size() != OTHER_ARANET_DATA_LENGTH) {
|
||||
if ((this->warned_ & WARNED_INVALID_LENGTH) == 0) {
|
||||
ESP_LOGW(TAG, "Unexpected manufacturer data length: %u", static_cast<unsigned>(data.size()));
|
||||
this->warned_ |= WARNED_INVALID_LENGTH;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Aranet4 has no device type byte, so its flags and trailer start one byte earlier
|
||||
const bool is_aranet4 = data.size() == ARANET4_DATA_LENGTH;
|
||||
if ((data[is_aranet4 ? 0 : 1] & 0x20) == 0) {
|
||||
if ((this->warned_ & WARNED_INTEGRATION_DISABLED) == 0) {
|
||||
ESP_LOGW(TAG, "Smart Home integration is disabled on the Aranet device");
|
||||
this->warned_ |= WARNED_INTEGRATION_DISABLED;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_aranet4 && (data[0] == 0 || data[0] > 3)) {
|
||||
if ((this->warned_ & WARNED_UNSUPPORTED_TYPE) == 0) {
|
||||
ESP_LOGW(TAG, "Unsupported Aranet device type: %u", data[0]);
|
||||
this->warned_ |= WARNED_UNSUPPORTED_TYPE;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const auto device_type =
|
||||
is_aranet4 ? AranetDeviceType::ARANET_DEVICE_TYPE_ARANET4 : static_cast<AranetDeviceType>(data[0]);
|
||||
|
||||
// Battery, measurement interval, measurement age and measurement counter
|
||||
const uint8_t *trailer = &data[is_aranet4 ? 15 : 17];
|
||||
const uint16_t measurement_interval = encode_uint16(trailer[3], trailer[2]);
|
||||
const uint16_t measurement_age = encode_uint16(trailer[5], trailer[4]);
|
||||
const uint8_t measurement_counter = trailer[6];
|
||||
|
||||
publish_if_set(this->measurement_age_sensor_, measurement_age);
|
||||
publish_if_set(this->signal_strength_sensor_, device.get_rssi());
|
||||
|
||||
if (measurement_counter == this->last_measurement_counter_)
|
||||
return true;
|
||||
this->last_measurement_counter_ = measurement_counter;
|
||||
|
||||
// Not meaningful for Aranet Radiation, which stores its dose duration in these bytes
|
||||
const float temperature = encode_uint16(data[11], data[10]) / 20.0f;
|
||||
const float pressure = encode_uint16(data[13], data[12]) / 10.0f;
|
||||
switch (device_type) {
|
||||
case AranetDeviceType::ARANET_DEVICE_TYPE_ARANET4: {
|
||||
const uint16_t co2 = encode_uint16(data[9], data[8]);
|
||||
publish_if_set(this->co2_sensor_, co2);
|
||||
publish_if_set(this->temperature_sensor_, temperature);
|
||||
publish_if_set(this->pressure_sensor_, pressure);
|
||||
publish_if_set(this->humidity_sensor_, data[14]);
|
||||
ESP_LOGD(TAG, "Aranet4 CO2: %u ppm, temperature: %.1f C, humidity: %u%%, pressure: %.1f hPa", co2, temperature,
|
||||
data[14], pressure);
|
||||
break;
|
||||
}
|
||||
case AranetDeviceType::ARANET_DEVICE_TYPE_ARANET2: {
|
||||
const float humidity = encode_uint16(data[15], data[14]) / 10.0f;
|
||||
publish_if_set(this->temperature_sensor_, temperature);
|
||||
publish_if_set(this->humidity_sensor_, humidity);
|
||||
ESP_LOGD(TAG, "Aranet2 temperature: %.1f C, humidity: %.1f%%", temperature, humidity);
|
||||
break;
|
||||
}
|
||||
case AranetDeviceType::ARANET_DEVICE_TYPE_RADIATION: {
|
||||
const uint32_t radiation_total = encode_uint32(data[9], data[8], data[7], data[6]);
|
||||
const uint32_t radiation_duration = encode_uint32(data[13], data[12], data[11], data[10]);
|
||||
const uint16_t radiation_rate = encode_uint16(data[15], data[14]);
|
||||
publish_if_set(this->radiation_rate_sensor_, radiation_rate / 1000.0f);
|
||||
publish_if_set(this->radiation_total_sensor_, radiation_total);
|
||||
publish_if_set(this->radiation_duration_sensor_, radiation_duration);
|
||||
ESP_LOGD(TAG, "Aranet Radiation dose rate: %u nSv/h, total dose: %" PRIu32 " nSv", radiation_rate,
|
||||
radiation_total);
|
||||
break;
|
||||
}
|
||||
case AranetDeviceType::ARANET_DEVICE_TYPE_RADON: {
|
||||
const uint16_t radon = encode_uint16(data[9], data[8]);
|
||||
const float humidity = encode_uint16(data[15], data[14]) / 10.0f;
|
||||
publish_if_set(this->radon_sensor_, radon);
|
||||
// Aranet Radon One uses zero temperature and humidity to indicate fields not supported by that model.
|
||||
if (temperature != 0)
|
||||
publish_if_set(this->temperature_sensor_, temperature);
|
||||
publish_if_set(this->pressure_sensor_, pressure);
|
||||
if (humidity != 0)
|
||||
publish_if_set(this->humidity_sensor_, humidity);
|
||||
ESP_LOGD(TAG, "Aranet Radon: %u Bq/m3, temperature: %.1f C, humidity: %.1f%%, pressure: %.1f hPa", radon,
|
||||
temperature, humidity, pressure);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
publish_if_set(this->battery_level_sensor_, trailer[0]);
|
||||
publish_if_set(this->measurement_interval_sensor_, measurement_interval);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace esphome::aranet
|
||||
@@ -1,42 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_device.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::aranet {
|
||||
|
||||
class Aranet final : public Component, public ble_device_base::ESPBTDeviceListener {
|
||||
public:
|
||||
explicit Aranet(uint64_t address) : address_(address) {}
|
||||
|
||||
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t WARNED_INVALID_LENGTH = 1 << 0;
|
||||
static constexpr uint8_t WARNED_INTEGRATION_DISABLED = 1 << 1;
|
||||
static constexpr uint8_t WARNED_UNSUPPORTED_TYPE = 1 << 2;
|
||||
|
||||
// Small state first so it fills the padding before the first pointer
|
||||
uint16_t last_measurement_counter_{0x100}; // Out of range of the 8-bit counter until the first measurement
|
||||
uint8_t warned_{0};
|
||||
|
||||
SUB_SENSOR(co2)
|
||||
SUB_SENSOR(temperature)
|
||||
SUB_SENSOR(humidity)
|
||||
SUB_SENSOR(pressure)
|
||||
SUB_SENSOR(battery_level)
|
||||
SUB_SENSOR(measurement_interval)
|
||||
SUB_SENSOR(measurement_age)
|
||||
SUB_SENSOR(signal_strength)
|
||||
SUB_SENSOR(radon)
|
||||
SUB_SENSOR(radiation_rate)
|
||||
SUB_SENSOR(radiation_total)
|
||||
SUB_SENSOR(radiation_duration)
|
||||
|
||||
protected:
|
||||
uint64_t address_;
|
||||
};
|
||||
|
||||
} // namespace esphome::aranet
|
||||
@@ -1,150 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import ble_device_base, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_CO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_MAC_ADDRESS,
|
||||
CONF_PRESSURE,
|
||||
CONF_RADON,
|
||||
CONF_SIGNAL_STRENGTH,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_RADON,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_RADIOACTIVE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_DECIBEL_MILLIWATT,
|
||||
UNIT_HECTOPASCAL,
|
||||
UNIT_MICROSILVERTS_PER_HOUR,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
UNIT_SECOND,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["ble_device_base"]
|
||||
|
||||
CONF_MEASUREMENT_AGE = "measurement_age"
|
||||
CONF_MEASUREMENT_INTERVAL = "measurement_interval"
|
||||
CONF_RADIATION_RATE = "radiation_rate"
|
||||
CONF_RADIATION_TOTAL = "radiation_total"
|
||||
CONF_RADIATION_DURATION = "radiation_duration"
|
||||
UNIT_NANOSIEVERT = "nSv"
|
||||
|
||||
aranet_ns = cg.esphome_ns.namespace("aranet")
|
||||
Aranet = aranet_ns.class_("Aranet", ble_device_base.ESPBTDeviceListener, cg.Component)
|
||||
|
||||
DIAGNOSTIC_DURATION_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Aranet),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_CO2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
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,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_MEASUREMENT_INTERVAL): DIAGNOSTIC_DURATION_SCHEMA,
|
||||
cv.Optional(CONF_MEASUREMENT_AGE): DIAGNOSTIC_DURATION_SCHEMA,
|
||||
cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_DECIBEL_MILLIWATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_RADON): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_RADON,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_RADIATION_RATE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROSILVERTS_PER_HOUR,
|
||||
accuracy_decimals=3,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_RADIATION_TOTAL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_NANOSIEVERT,
|
||||
accuracy_decimals=0,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional(CONF_RADIATION_DURATION): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(ble_device_base.BLE_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_MAC_ADDRESS].as_hex)
|
||||
await cg.register_component(var, config)
|
||||
await ble_device_base.register_ble_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_CO2, var.set_co2_sensor)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
|
||||
await sensors(CONF_PRESSURE, var.set_pressure_sensor)
|
||||
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level_sensor)
|
||||
await sensors(CONF_MEASUREMENT_INTERVAL, var.set_measurement_interval_sensor)
|
||||
await sensors(CONF_MEASUREMENT_AGE, var.set_measurement_age_sensor)
|
||||
await sensors(CONF_SIGNAL_STRENGTH, var.set_signal_strength_sensor)
|
||||
await sensors(CONF_RADON, var.set_radon_sensor)
|
||||
await sensors(CONF_RADIATION_RATE, var.set_radiation_rate_sensor)
|
||||
await sensors(CONF_RADIATION_TOTAL, var.set_radiation_total_sensor)
|
||||
await sensors(CONF_RADIATION_DURATION, var.set_radiation_duration_sensor)
|
||||
@@ -391,7 +391,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Adds a define and IDF component for legacy `audio_decoder.cpp`.
|
||||
if data.flac_support:
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.3.0")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.2.0")
|
||||
_emit_memory_pair(
|
||||
data.flac.buffer_memory,
|
||||
"CONFIG_MICRO_FLAC_PREFER_PSRAM",
|
||||
|
||||
@@ -3,7 +3,6 @@ from pathlib import Path
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, media_source, psram
|
||||
from esphome.components.const import CONF_VERIFY_SSL
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
||||
import esphome.final_validate as fv
|
||||
@@ -17,6 +16,7 @@ AUTO_LOAD = ["audio"]
|
||||
CONF_PERSISTENT_RING_BUFFER = "persistent_ring_buffer"
|
||||
CONF_CA_CERTIFICATE_PATH = "ca_certificate_path"
|
||||
CONF_HTTP_REQUEST = "http_request"
|
||||
CONF_VERIFY_SSL = "verify_ssl"
|
||||
|
||||
audio_http_ns = cg.esphome_ns.namespace("audio_http")
|
||||
AudioHTTPMediaSource = audio_http_ns.class_(
|
||||
|
||||
@@ -146,7 +146,10 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t checksum = sum_bytes(remote_state, BALLU_STATE_LENGTH - 1);
|
||||
uint8_t checksum = 0;
|
||||
// Calculate checksum and compare with signal value.
|
||||
for (uint8_t i = 0; i < BALLU_STATE_LENGTH - 1; i++)
|
||||
checksum += remote_state[i];
|
||||
|
||||
if (checksum != remote_state[BALLU_STATE_LENGTH - 1]) {
|
||||
ESP_LOGVV(TAG, "Checksum fail");
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_GAIN,
|
||||
CONF_GLASS_ATTENUATION_FACTOR,
|
||||
CONF_ID,
|
||||
CONF_INTEGRATION_TIME,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@latonita"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "bh1745"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_BH1745_ID = "bh1745_id"
|
||||
|
||||
bh1745_ns = cg.esphome_ns.namespace("bh1745")
|
||||
|
||||
BH1745Component = bh1745_ns.class_(
|
||||
"BH1745Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
AdcGain = bh1745_ns.enum("AdcGain", is_class=True)
|
||||
ADC_GAINS = {
|
||||
"1X": AdcGain.ADC_GAIN_1X,
|
||||
"2X": AdcGain.ADC_GAIN_2X,
|
||||
"16X": AdcGain.ADC_GAIN_16X,
|
||||
}
|
||||
|
||||
MeasurementTime = bh1745_ns.enum("MeasurementTime", is_class=True)
|
||||
MEASUREMENT_TIMES = {
|
||||
160: MeasurementTime.MEASUREMENT_TIME_160MS,
|
||||
320: MeasurementTime.MEASUREMENT_TIME_320MS,
|
||||
640: MeasurementTime.MEASUREMENT_TIME_640MS,
|
||||
1280: MeasurementTime.MEASUREMENT_TIME_1280MS,
|
||||
2560: MeasurementTime.MEASUREMENT_TIME_2560MS,
|
||||
5120: MeasurementTime.MEASUREMENT_TIME_5120MS,
|
||||
}
|
||||
|
||||
|
||||
def validate_measurement_time(value: Any) -> Any:
|
||||
value = cv.positive_time_period_milliseconds(value).total_milliseconds
|
||||
return cv.enum(MEASUREMENT_TIMES, int=True)(value)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BH1745Component),
|
||||
cv.Optional(CONF_GAIN, default="1X"): cv.enum(ADC_GAINS, upper=True),
|
||||
cv.Optional(
|
||||
CONF_INTEGRATION_TIME, default="160ms"
|
||||
): validate_measurement_time,
|
||||
cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.float_range(
|
||||
min=1.0
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x38))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_adc_gain(config[CONF_GAIN]))
|
||||
cg.add(var.set_measurement_time(config[CONF_INTEGRATION_TIME]))
|
||||
cg.add(var.set_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR]))
|
||||
@@ -1,182 +0,0 @@
|
||||
#include "bh1745.h"
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bh1745 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bh1745");
|
||||
|
||||
static constexpr uint8_t REG_SYSTEM_CONTROL = 0x40;
|
||||
static constexpr uint8_t REG_MODE_CONTROL1 = 0x41;
|
||||
static constexpr uint8_t REG_MODE_CONTROL2 = 0x42;
|
||||
static constexpr uint8_t REG_MODE_CONTROL3 = 0x44;
|
||||
static constexpr uint8_t REG_RED_DATA_LSB = 0x50;
|
||||
static constexpr uint8_t REG_INTERRUPT = 0x60;
|
||||
static constexpr uint8_t REG_TH_LSB = 0x62;
|
||||
static constexpr uint8_t REG_MANUFACTURER_ID = 0x92;
|
||||
|
||||
static constexpr uint8_t MANUFACTURER_ID = 0xE0;
|
||||
static constexpr uint8_t PART_ID = 0b001011;
|
||||
static constexpr uint8_t SYSTEM_CONTROL_PART_ID_MASK = 0x3F;
|
||||
static constexpr uint8_t SYSTEM_CONTROL_SW_RESET = 0x80;
|
||||
static constexpr uint8_t MODE_CONTROL2_RGBC_EN = 0x10;
|
||||
static constexpr uint8_t MODE_CONTROL2_VALID = 0x80;
|
||||
static constexpr uint8_t MODE_CONTROL3_VALUE = 0x02; // the datasheet says always write 0x02
|
||||
static constexpr uint8_t INTERRUPT_ENABLE = 0x01;
|
||||
|
||||
static constexpr uint8_t BH1745_CHANNELS = 4;
|
||||
static constexpr uint32_t RESET_DELAY_MS = 100;
|
||||
static constexpr uint32_t BASE_MEAS_TIME_MS = 160;
|
||||
|
||||
static uint32_t get_measurement_time_ms(MeasurementTime time) {
|
||||
return BASE_MEAS_TIME_MS << static_cast<uint8_t>(time);
|
||||
}
|
||||
|
||||
static uint8_t get_adc_gain(AdcGain gain) {
|
||||
return gain == AdcGain::ADC_GAIN_16X ? 16 : 1 + static_cast<uint8_t>(gain);
|
||||
}
|
||||
|
||||
static float calculate_lux(uint16_t red, uint16_t green, bool low_clear_ratio, float scale) {
|
||||
if (green == 0)
|
||||
return 0.0f;
|
||||
float lx = low_clear_ratio ? 0.202f * red + 0.766f * green : 0.159f * red + 0.646f * green;
|
||||
return lx * scale;
|
||||
}
|
||||
|
||||
static float calculate_cct(uint16_t red, uint16_t green, uint16_t blue, bool low_clear_ratio) {
|
||||
if (green == 0)
|
||||
return 0.0f;
|
||||
float all = static_cast<float>(static_cast<uint32_t>(red) + green + blue);
|
||||
float r_ratio = red / all;
|
||||
float b_ratio = blue / all;
|
||||
float ct;
|
||||
if (low_clear_ratio) {
|
||||
float b_eff = std::fmin(b_ratio * 3.13f, 1.0f);
|
||||
ct = ((1.0f - b_eff) * 12746.0f * std::exp(-2.911f * r_ratio)) + (b_eff * 1637.0f * std::exp(4.865f * b_ratio));
|
||||
} else {
|
||||
float b_eff = std::fmin(b_ratio * 10.67f, 1.0f);
|
||||
ct = ((1.0f - b_eff) * 16234.0f * std::exp(-2.781f * r_ratio)) + (b_eff * 1882.0f * std::exp(4.448f * b_ratio));
|
||||
}
|
||||
return clamp_at_most(ct, 10000.0f);
|
||||
}
|
||||
|
||||
void BH1745Component::setup() {
|
||||
uint8_t manufacturer_id;
|
||||
if (!this->read_byte(REG_MANUFACTURER_ID, &manufacturer_id)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (manufacturer_id != MANUFACTURER_ID) {
|
||||
this->mark_failed(LOG_STR("Wrong manufacturer ID"));
|
||||
return;
|
||||
}
|
||||
uint8_t system_control;
|
||||
if (!this->read_byte(REG_SYSTEM_CONTROL, &system_control) ||
|
||||
(system_control & SYSTEM_CONTROL_PART_ID_MASK) != PART_ID) {
|
||||
this->mark_failed(LOG_STR("Wrong part ID"));
|
||||
return;
|
||||
}
|
||||
if (!this->write_byte(REG_SYSTEM_CONTROL, SYSTEM_CONTROL_SW_RESET)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Light lost behind glass is scaled back up, as the other light sensors do
|
||||
this->lux_scale_ = 160.0f * this->glass_attenuation_factor_ /
|
||||
(get_adc_gain(this->adc_gain_) * get_measurement_time_ms(this->measurement_time_));
|
||||
|
||||
this->set_timeout(RESET_DELAY_MS, [this]() {
|
||||
if (this->interrupt_output_) {
|
||||
// High threshold 0 and low threshold 0xFFFF (low byte first) make the interrupt condition always
|
||||
// true, so the INT pin simply follows the enable bit, which drives the breakout's LEDs.
|
||||
static constexpr uint8_t THRESHOLDS[4] = {0x00, 0x00, 0xFF, 0xFF};
|
||||
if (!this->write_bytes(REG_TH_LSB, THRESHOLDS, sizeof(THRESHOLDS))) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// RGBC_EN makes the chip measure continuously; update() reads the latest result.
|
||||
if (!this->write_byte(REG_MODE_CONTROL1, static_cast<uint8_t>(this->measurement_time_)) ||
|
||||
!this->write_byte(REG_MODE_CONTROL2, static_cast<uint8_t>(this->adc_gain_) | MODE_CONTROL2_RGBC_EN) ||
|
||||
!this->write_byte(REG_MODE_CONTROL3, MODE_CONTROL3_VALUE)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->busy_ = false;
|
||||
});
|
||||
}
|
||||
|
||||
void BH1745Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"BH1745:\n"
|
||||
" Gain: %ux\n"
|
||||
" Integration time: %" PRIu32 " ms\n"
|
||||
" Glass attenuation factor: %.2f",
|
||||
get_adc_gain(this->adc_gain_), get_measurement_time_ms(this->measurement_time_),
|
||||
this->glass_attenuation_factor_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
LOG_SENSOR(" ", "Red Counts", this->red_counts_sensor_);
|
||||
LOG_SENSOR(" ", "Green Counts", this->green_counts_sensor_);
|
||||
LOG_SENSOR(" ", "Blue Counts", this->blue_counts_sensor_);
|
||||
LOG_SENSOR(" ", "Clear Counts", this->clear_counts_sensor_);
|
||||
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
|
||||
LOG_SENSOR(" ", "Color Temperature", this->color_temperature_sensor_);
|
||||
}
|
||||
|
||||
void BH1745Component::update() {
|
||||
if (this->busy_)
|
||||
return;
|
||||
|
||||
// Reading MODE_CONTROL2 clears VALID, so read it only once per poll.
|
||||
uint8_t mode_control2;
|
||||
if (!this->read_byte(REG_MODE_CONTROL2, &mode_control2)) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if ((mode_control2 & MODE_CONTROL2_VALID) == 0)
|
||||
return;
|
||||
|
||||
uint8_t buffer[BH1745_CHANNELS * 2];
|
||||
if (!this->read_bytes(REG_RED_DATA_LSB, buffer, sizeof(buffer))) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
|
||||
// Each channel is sent low byte first.
|
||||
const uint16_t red = encode_uint16(buffer[1], buffer[0]);
|
||||
const uint16_t green = encode_uint16(buffer[3], buffer[2]);
|
||||
const uint16_t blue = encode_uint16(buffer[5], buffer[4]);
|
||||
const uint16_t clear = encode_uint16(buffer[7], buffer[6]);
|
||||
ESP_LOGV(TAG, "Red: %u, Green: %u, Blue: %u, Clear: %u", red, green, blue, clear);
|
||||
|
||||
if (this->red_counts_sensor_ != nullptr)
|
||||
this->red_counts_sensor_->publish_state(red);
|
||||
if (this->green_counts_sensor_ != nullptr)
|
||||
this->green_counts_sensor_->publish_state(green);
|
||||
if (this->blue_counts_sensor_ != nullptr)
|
||||
this->blue_counts_sensor_->publish_state(blue);
|
||||
if (this->clear_counts_sensor_ != nullptr)
|
||||
this->clear_counts_sensor_->publish_state(clear);
|
||||
|
||||
// The datasheet picks its coefficients by whether clear / green < 0.160 (= 4 / 25).
|
||||
const bool low_clear_ratio = static_cast<uint32_t>(clear) * 25 < static_cast<uint32_t>(green) * 4;
|
||||
if (this->illuminance_sensor_ != nullptr)
|
||||
this->illuminance_sensor_->publish_state(calculate_lux(red, green, low_clear_ratio, this->lux_scale_));
|
||||
if (this->color_temperature_sensor_ != nullptr)
|
||||
this->color_temperature_sensor_->publish_state(calculate_cct(red, green, blue, low_clear_ratio));
|
||||
}
|
||||
|
||||
void BH1745Component::set_interrupt_state(bool on) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (!this->write_byte(REG_INTERRUPT, on ? INTERRUPT_ENABLE : 0x00)) {
|
||||
this->status_set_warning();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::bh1745
|
||||
@@ -1,54 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::bh1745 {
|
||||
|
||||
enum class MeasurementTime : uint8_t {
|
||||
MEASUREMENT_TIME_160MS = 0b000,
|
||||
MEASUREMENT_TIME_320MS = 0b001,
|
||||
MEASUREMENT_TIME_640MS = 0b010,
|
||||
MEASUREMENT_TIME_1280MS = 0b011,
|
||||
MEASUREMENT_TIME_2560MS = 0b100,
|
||||
MEASUREMENT_TIME_5120MS = 0b101,
|
||||
};
|
||||
|
||||
enum class AdcGain : uint8_t {
|
||||
ADC_GAIN_1X = 0,
|
||||
ADC_GAIN_2X = 1,
|
||||
ADC_GAIN_16X = 2,
|
||||
};
|
||||
|
||||
class BH1745Component : public PollingComponent, public i2c::I2CDevice {
|
||||
SUB_SENSOR(red_counts)
|
||||
SUB_SENSOR(green_counts)
|
||||
SUB_SENSOR(blue_counts)
|
||||
SUB_SENSOR(clear_counts)
|
||||
SUB_SENSOR(illuminance)
|
||||
SUB_SENSOR(color_temperature)
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void set_measurement_time(MeasurementTime measurement_time) { this->measurement_time_ = measurement_time; }
|
||||
void set_adc_gain(AdcGain adc_gain) { this->adc_gain_ = adc_gain; }
|
||||
void set_glass_attenuation_factor(float factor) { this->glass_attenuation_factor_ = factor; }
|
||||
|
||||
/// Called when an output platform uses the INT pin, so setup() pins the interrupt thresholds.
|
||||
void enable_interrupt_output() { this->interrupt_output_ = true; }
|
||||
void set_interrupt_state(bool on);
|
||||
|
||||
protected:
|
||||
float glass_attenuation_factor_{1.0f};
|
||||
float lux_scale_{1.0f}; // 160 / (gain * integration time * glass attenuation), set in setup()
|
||||
MeasurementTime measurement_time_{MeasurementTime::MEASUREMENT_TIME_160MS};
|
||||
AdcGain adc_gain_{AdcGain::ADC_GAIN_1X};
|
||||
bool busy_{true}; // cleared once the chip is configured after the reset
|
||||
bool interrupt_output_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::bh1745
|
||||
@@ -1,28 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_BH1745_ID, BH1745Component, bh1745_ns
|
||||
|
||||
DEPENDENCIES = ["bh1745"]
|
||||
|
||||
BH1745InterruptPinOutput = bh1745_ns.class_(
|
||||
"BH1745InterruptPinOutput", output.BinaryOutput
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BH1745InterruptPinOutput),
|
||||
cv.GenerateID(CONF_BH1745_ID): cv.use_id(BH1745Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await output.register_output(var, config)
|
||||
hub = await cg.get_variable(config[CONF_BH1745_ID])
|
||||
await cg.register_parented(var, hub)
|
||||
cg.add(hub.enable_interrupt_output())
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/output/binary_output.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "../bh1745.h"
|
||||
|
||||
namespace esphome::bh1745 {
|
||||
|
||||
/// Drives the BH1745 INT pin, which switches the LEDs on breakout boards such as the Pimoroni one.
|
||||
class BH1745InterruptPinOutput : public output::BinaryOutput, public Parented<BH1745Component> {
|
||||
protected:
|
||||
void write_state(bool state) override { this->parent_->set_interrupt_state(state); }
|
||||
};
|
||||
|
||||
} // namespace esphome::bh1745
|
||||
@@ -1,66 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.components.const import (
|
||||
CONF_BLUE_CHANNEL,
|
||||
CONF_CLEAR_CHANNEL,
|
||||
CONF_GREEN_CHANNEL,
|
||||
CONF_RED_CHANNEL,
|
||||
UNIT_COUNTS,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_COLOR_TEMPERATURE,
|
||||
CONF_ILLUMINANCE,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
ICON_LIGHTBULB,
|
||||
ICON_THERMOMETER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_KELVIN,
|
||||
UNIT_LUX,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_BH1745_ID, BH1745Component
|
||||
|
||||
DEPENDENCIES = ["bh1745"]
|
||||
|
||||
color_channel_schema = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNTS,
|
||||
icon=ICON_LIGHTBULB,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_BH1745_ID): cv.use_id(BH1745Component),
|
||||
cv.Optional(CONF_RED_CHANNEL): color_channel_schema,
|
||||
cv.Optional(CONF_GREEN_CHANNEL): color_channel_schema,
|
||||
cv.Optional(CONF_BLUE_CHANNEL): color_channel_schema,
|
||||
cv.Optional(CONF_CLEAR_CHANNEL): color_channel_schema,
|
||||
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_LUX,
|
||||
icon=ICON_LIGHTBULB,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_COLOR_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KELVIN,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_BH1745_ID])
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_RED_CHANNEL, hub.set_red_counts_sensor)
|
||||
await sensors(CONF_GREEN_CHANNEL, hub.set_green_counts_sensor)
|
||||
await sensors(CONF_BLUE_CHANNEL, hub.set_blue_counts_sensor)
|
||||
await sensors(CONF_CLEAR_CHANNEL, hub.set_clear_counts_sensor)
|
||||
await sensors(CONF_ILLUMINANCE, hub.set_illuminance_sensor)
|
||||
await sensors(CONF_COLOR_TEMPERATURE, hub.set_color_temperature_sensor)
|
||||
@@ -74,10 +74,6 @@ from esphome.types import ConfigType
|
||||
from esphome.util import Registry
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
DEFAULT_INVALID_COOLDOWN_MS = (
|
||||
1000 # Keep in sync with invalid_cooldown_ in automation.h
|
||||
)
|
||||
DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_BATTERY_CHARGING,
|
||||
@@ -135,9 +131,10 @@ ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.templa
|
||||
DoubleClickTrigger = binary_sensor_ns.class_(
|
||||
"DoubleClickTrigger", automation.Trigger.template()
|
||||
)
|
||||
MultiClickTrigger = binary_sensor_ns.class_(
|
||||
"MultiClickTrigger", automation.Trigger.template(), cg.Component
|
||||
MultiClickTriggerBase = binary_sensor_ns.class_(
|
||||
"MultiClickTriggerBase", automation.Trigger.template(), cg.Component
|
||||
)
|
||||
MultiClickTrigger = binary_sensor_ns.class_("MultiClickTrigger", MultiClickTriggerBase)
|
||||
MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
|
||||
|
||||
|
||||
@@ -149,6 +146,7 @@ DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter)
|
||||
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter)
|
||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter)
|
||||
AutorepeatFilterTiming = binary_sensor_ns.struct("AutorepeatFilterTiming")
|
||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
||||
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
|
||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter)
|
||||
@@ -236,53 +234,49 @@ async def delayed_off_filter_to_code(config, filter_id):
|
||||
return var
|
||||
|
||||
|
||||
AUTOREPEAT_TIMING_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_DELAY, default=DEFAULT_DELAY
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_OFF, default=DEFAULT_TIME_OFF
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_ON, default=DEFAULT_TIME_ON
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_filter(
|
||||
"autorepeat",
|
||||
AutorepeatFilter,
|
||||
cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_DELAY, default=DEFAULT_DELAY
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_OFF, default=DEFAULT_TIME_OFF
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_ON, default=DEFAULT_TIME_ON
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
cv.Length(max=254),
|
||||
),
|
||||
cv.All(cv.ensure_list(AUTOREPEAT_TIMING_SCHEMA), cv.Length(max=254)),
|
||||
)
|
||||
async def autorepeat_filter_to_code(config, filter_id):
|
||||
if len(config) > 0:
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||
("delay", conf[CONF_DELAY]),
|
||||
("time_off", conf[CONF_TIME_OFF]),
|
||||
("time_on", conf[CONF_TIME_ON]),
|
||||
)
|
||||
for conf in config
|
||||
]
|
||||
else:
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||
("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds),
|
||||
(
|
||||
"time_off",
|
||||
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
||||
),
|
||||
(
|
||||
"time_on",
|
||||
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
||||
),
|
||||
)
|
||||
]
|
||||
return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(timings)), timings)
|
||||
async def autorepeat_filter_to_code(config: list[ConfigType], filter_id: ID) -> MockObj:
|
||||
config = config or [AUTOREPEAT_TIMING_SCHEMA({})]
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
AutorepeatFilterTiming,
|
||||
("delay", conf[CONF_DELAY].total_milliseconds),
|
||||
("time_off", conf[CONF_TIME_OFF].total_milliseconds),
|
||||
("time_on", conf[CONF_TIME_ON].total_milliseconds),
|
||||
)
|
||||
for conf in config
|
||||
]
|
||||
# A never-run delay ends the table, so the filter stores no count.
|
||||
timings.append(
|
||||
cg.StructInitializer(
|
||||
AutorepeatFilterTiming, ("delay", cg.RawExpression("SCHEDULER_DONT_RUN"))
|
||||
)
|
||||
)
|
||||
table = cg.shared_progmem_array(
|
||||
"binary_sensor_autorepeat_timings",
|
||||
AutorepeatFilterTiming,
|
||||
cg.ArrayInitializer(*timings),
|
||||
)
|
||||
return cg.new_Pvariable(filter_id, table)
|
||||
|
||||
|
||||
@register_filter("lambda", LambdaFilter, cv.returning_lambda)
|
||||
@@ -495,8 +489,7 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
cv.Length(min=1, max=255),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_INVALID_COOLDOWN,
|
||||
default=f"{DEFAULT_INVALID_COOLDOWN_MS}ms",
|
||||
CONF_INVALID_COOLDOWN, default="1s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
@@ -590,14 +583,11 @@ async def _build_binary_sensor_automations(var, config):
|
||||
)
|
||||
for tim in conf[CONF_TIMING]
|
||||
]
|
||||
table = cg.shared_progmem_array(
|
||||
"multi_click_timing", MultiClickTriggerEvent, cg.ArrayInitializer(*timings)
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], cg.TemplateArguments(len(timings)), var, timings
|
||||
)
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, table, len(timings))
|
||||
if (
|
||||
cooldown := conf[CONF_INVALID_COOLDOWN]
|
||||
).total_milliseconds != DEFAULT_INVALID_COOLDOWN_MS:
|
||||
cg.add(trigger.set_invalid_cooldown(cooldown))
|
||||
if CONF_INVALID_COOLDOWN in conf:
|
||||
cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN]))
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@@ -614,8 +604,7 @@ async def setup_binary_sensor_core_(var, config):
|
||||
if filters_config := config.get(CONF_FILTERS):
|
||||
cg.add_define("USE_BINARY_SENSOR_FILTER")
|
||||
filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config)
|
||||
for filter_ in filters:
|
||||
cg.add(var.add_filter(filter_))
|
||||
cg.add(var.add_filters(filters))
|
||||
|
||||
CORE.add_job(_build_binary_sensor_automations, var, config)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ constexpr uint32_t MULTICLICK_COOLDOWN_ID = 1;
|
||||
constexpr uint32_t MULTICLICK_IS_VALID_ID = 2;
|
||||
constexpr uint32_t MULTICLICK_IS_NOT_VALID_ID = 3;
|
||||
|
||||
void MultiClickTrigger::on_state_(bool state) {
|
||||
void MultiClickTriggerBase::on_state_(bool state) {
|
||||
// Handle duplicate events
|
||||
if (state == this->last_state_) {
|
||||
return;
|
||||
@@ -30,7 +30,7 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->at_index_ == 0) {
|
||||
if (!this->at_index_.has_value()) {
|
||||
// Start matching
|
||||
MultiClickTriggerEvent evt = this->timing_[0];
|
||||
if (evt.state == state) {
|
||||
@@ -55,7 +55,8 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t at_index = this->at_index_;
|
||||
// at_index_ has a value here (the !has_value() branch above returns).
|
||||
size_t at_index = *this->at_index_;
|
||||
if (at_index == this->timing_count_) {
|
||||
this->trigger_();
|
||||
return;
|
||||
@@ -64,15 +65,15 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
MultiClickTriggerEvent evt = this->timing_[at_index];
|
||||
|
||||
if (evt.max_length != 4294967294UL) {
|
||||
ESP_LOGV(TAG, "A i=%u min=%" PRIu32 " max=%" PRIu32, at_index, evt.min_length, evt.max_length);
|
||||
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, at_index, evt.min_length, evt.max_length); // NOLINT
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
this->schedule_is_not_valid_(evt.max_length);
|
||||
} else if (at_index + 1 != this->timing_count_) {
|
||||
ESP_LOGV(TAG, "B i=%u min=%" PRIu32, at_index, evt.min_length);
|
||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "C i=%u min=%" PRIu32, at_index, evt.min_length);
|
||||
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
|
||||
this->is_valid_ = false;
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||
@@ -80,19 +81,19 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
|
||||
this->at_index_ = at_index + 1;
|
||||
}
|
||||
void MultiClickTrigger::schedule_cooldown_() {
|
||||
void MultiClickTriggerBase::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
this->is_in_cooldown_ = true;
|
||||
this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again.");
|
||||
this->is_in_cooldown_ = false;
|
||||
});
|
||||
this->at_index_ = 0;
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) {
|
||||
if (min_length == 0) {
|
||||
this->is_valid_ = true;
|
||||
return;
|
||||
@@ -104,7 +105,7 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
this->is_valid_ = true;
|
||||
});
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.",
|
||||
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
|
||||
@@ -112,14 +113,14 @@ void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->schedule_cooldown_();
|
||||
});
|
||||
}
|
||||
void MultiClickTrigger::cancel() {
|
||||
void MultiClickTriggerBase::cancel() {
|
||||
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
}
|
||||
void MultiClickTrigger::trigger_() {
|
||||
void MultiClickTriggerBase::trigger_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
||||
this->at_index_ = 0;
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
@@ -13,14 +13,10 @@
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
struct MultiClickTriggerEvent {
|
||||
uint32_t state; // Not bool: all-word fields allow aligned loads from PROGMEM on ESP8266
|
||||
bool state;
|
||||
uint32_t min_length;
|
||||
uint32_t max_length;
|
||||
};
|
||||
// Read straight from PROGMEM; a byte-sized field would fault on ESP8266.
|
||||
static_assert(std::is_same_v<decltype(MultiClickTriggerEvent::state), uint32_t> &&
|
||||
sizeof(MultiClickTriggerEvent) == 3 * sizeof(uint32_t),
|
||||
"MultiClickTriggerEvent fields must all be uint32_t");
|
||||
|
||||
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length);
|
||||
|
||||
@@ -74,11 +70,10 @@ class DoubleClickTrigger final : public Trigger<> {
|
||||
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
|
||||
};
|
||||
|
||||
class MultiClickTrigger final : public Trigger<>, public Component {
|
||||
/// Non-template base for MultiClickTrigger (keeps large method bodies out of the header).
|
||||
class MultiClickTriggerBase : public Trigger<>, public Component {
|
||||
public:
|
||||
/// `timing` is a codegen PROGMEM table shared by triggers with the same timing.
|
||||
MultiClickTrigger(BinarySensor *parent, const MultiClickTriggerEvent *timing, uint8_t timing_count)
|
||||
: parent_(parent), timing_(timing), timing_count_(timing_count) {}
|
||||
explicit MultiClickTriggerBase(BinarySensor *parent) : parent_(parent) {}
|
||||
|
||||
void setup() override {
|
||||
this->last_state_ = this->parent_->get_state_default(false);
|
||||
@@ -90,8 +85,8 @@ class MultiClickTrigger final : public Trigger<>, public Component {
|
||||
void set_invalid_cooldown(uint32_t invalid_cooldown) { this->invalid_cooldown_ = invalid_cooldown; }
|
||||
|
||||
void cancel();
|
||||
MultiClickTrigger(const MultiClickTrigger &) = delete;
|
||||
MultiClickTrigger &operator=(const MultiClickTrigger &) = delete;
|
||||
MultiClickTriggerBase(const MultiClickTriggerBase &) = delete;
|
||||
MultiClickTriggerBase &operator=(const MultiClickTriggerBase &) = delete;
|
||||
|
||||
protected:
|
||||
void on_state_(bool state);
|
||||
@@ -101,13 +96,28 @@ class MultiClickTrigger final : public Trigger<>, public Component {
|
||||
void trigger_();
|
||||
|
||||
BinarySensor *parent_;
|
||||
const MultiClickTriggerEvent *timing_;
|
||||
uint32_t invalid_cooldown_{1000}; // Must match DEFAULT_INVALID_COOLDOWN_MS in __init__.py
|
||||
uint8_t timing_count_;
|
||||
uint8_t at_index_{0}; // 0: not matching, otherwise the index of the next timing event
|
||||
const MultiClickTriggerEvent *timing_{nullptr};
|
||||
uint32_t invalid_cooldown_{1000};
|
||||
optional<size_t> at_index_{};
|
||||
uint8_t timing_count_{0};
|
||||
bool last_state_{false};
|
||||
bool is_in_cooldown_{false};
|
||||
bool is_valid_{false};
|
||||
};
|
||||
|
||||
/// Template wrapper that provides inline std::array storage for timing events.
|
||||
/// N is set by code generation to match the exact number of timing events configured in YAML.
|
||||
template<size_t N> class MultiClickTrigger final : public MultiClickTriggerBase {
|
||||
public:
|
||||
MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing)
|
||||
: MultiClickTriggerBase(parent) {
|
||||
init_array_from(this->timing_storage_, timing);
|
||||
this->timing_ = this->timing_storage_.data();
|
||||
this->timing_count_ = N;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::array<MultiClickTriggerEvent, N> timing_storage_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -63,12 +63,12 @@ optional<bool> DelayedOffFilter::new_value(bool value) {
|
||||
|
||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
||||
|
||||
// AutorepeatFilterBase
|
||||
// AutorepeatFilter
|
||||
// Two independent timers per instance, keyed off two stable addresses inside
|
||||
// the filter: `this` for the timing-step timer, `&active_timing_` for the
|
||||
// on/off timer. Both are unique per instance and don't collide with anything
|
||||
// else, so the self-keyed scheduler API is sufficient.
|
||||
optional<bool> AutorepeatFilterBase::new_value(bool value) {
|
||||
optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
if (this->active_timing_ != 0)
|
||||
return {};
|
||||
@@ -82,18 +82,18 @@ optional<bool> AutorepeatFilterBase::new_value(bool value) {
|
||||
}
|
||||
}
|
||||
|
||||
void AutorepeatFilterBase::next_timing_() {
|
||||
if (this->active_timing_ < this->timings_count_) {
|
||||
App.scheduler.set_timeout(this, this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); });
|
||||
}
|
||||
if (this->active_timing_ <= this->timings_count_) {
|
||||
this->active_timing_++;
|
||||
void AutorepeatFilter::next_timing_() {
|
||||
const uint32_t delay = this->timings_[this->active_timing_].delay;
|
||||
if (delay != SCHEDULER_DONT_RUN) {
|
||||
App.scheduler.set_timeout(this, delay, [this]() { this->next_timing_(); });
|
||||
}
|
||||
// Only reached with active_timing_ at most the end-marker index, so this never passes it.
|
||||
this->active_timing_++;
|
||||
if (this->active_timing_ == 2)
|
||||
this->next_value_(false);
|
||||
}
|
||||
|
||||
void AutorepeatFilterBase::next_value_(bool val) {
|
||||
void AutorepeatFilter::next_value_(bool val) {
|
||||
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
|
||||
this->output(val);
|
||||
App.scheduler.set_timeout(&this->active_timing_, val ? timing.time_on : timing.time_off,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BINARY_SENSOR_FILTER
|
||||
|
||||
#include <array>
|
||||
#include <type_traits>
|
||||
#ifdef USE_BINARY_SENSOR_FILTER
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
@@ -87,39 +87,34 @@ struct AutorepeatFilterTiming {
|
||||
uint32_t time_off;
|
||||
uint32_t time_on;
|
||||
};
|
||||
// Read straight from flash on ESP8266, so every field must stay a word.
|
||||
static_assert(std::is_same_v<decltype(AutorepeatFilterTiming::delay), uint32_t>,
|
||||
"AutorepeatFilterTiming fields must stay uint32_t");
|
||||
static_assert(std::is_same_v<decltype(AutorepeatFilterTiming::time_off), uint32_t>,
|
||||
"AutorepeatFilterTiming fields must stay uint32_t");
|
||||
static_assert(std::is_same_v<decltype(AutorepeatFilterTiming::time_on), uint32_t>,
|
||||
"AutorepeatFilterTiming fields must stay uint32_t");
|
||||
static_assert(sizeof(AutorepeatFilterTiming) == 3 * sizeof(uint32_t),
|
||||
"AutorepeatFilterTiming is read from flash with word loads");
|
||||
|
||||
/// Non-template base for AutorepeatFilter — all methods in filter.cpp.
|
||||
/// Lambdas capture this base pointer, so set_timeout/cancel_timeout are instantiated once.
|
||||
/// Timings live in a PROGMEM table emitted by codegen, ended by an entry whose delay is
|
||||
/// SCHEDULER_DONT_RUN (a step that could never advance anyway). Aligned loads are ESP8266 safe.
|
||||
/// The two scheduled timers are keyed off `this` and `&active_timing_`; since the address
|
||||
/// of `active_timing_` is taken as a scheduler key, the class must not be copied or moved.
|
||||
class AutorepeatFilterBase : public Filter {
|
||||
class AutorepeatFilter : public Filter {
|
||||
public:
|
||||
explicit AutorepeatFilter(const AutorepeatFilterTiming *timings) : timings_(timings) {}
|
||||
AutorepeatFilter(const AutorepeatFilter &) = delete;
|
||||
AutorepeatFilter &operator=(const AutorepeatFilter &) = delete;
|
||||
|
||||
optional<bool> new_value(bool value) override;
|
||||
AutorepeatFilterBase(const AutorepeatFilterBase &) = delete;
|
||||
AutorepeatFilterBase &operator=(const AutorepeatFilterBase &) = delete;
|
||||
|
||||
protected:
|
||||
AutorepeatFilterBase() = default;
|
||||
void next_timing_();
|
||||
void next_value_(bool val);
|
||||
|
||||
const AutorepeatFilterTiming *timings_{nullptr};
|
||||
uint8_t timings_count_{0};
|
||||
uint8_t active_timing_{0};
|
||||
};
|
||||
|
||||
/// Template wrapper that provides inline std::array storage for timings.
|
||||
/// N is set by code generation to match the exact number of timings configured in YAML.
|
||||
template<size_t N> class AutorepeatFilter : public AutorepeatFilterBase {
|
||||
public:
|
||||
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) {
|
||||
init_array_from(this->timings_storage_, timings);
|
||||
this->timings_ = this->timings_storage_.data();
|
||||
this->timings_count_ = N;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::array<AutorepeatFilterTiming, N> timings_storage_{};
|
||||
uint8_t active_timing_{0}; // before timings_ so it fills the padding after the Filter base
|
||||
const AutorepeatFilterTiming *timings_;
|
||||
};
|
||||
|
||||
class LambdaFilter : public Filter {
|
||||
|
||||
@@ -51,8 +51,12 @@ void BL0939::loop() {
|
||||
}
|
||||
|
||||
bool BL0939::validate_checksum(const DataPacket *data) {
|
||||
uint8_t checksum = BL0939_READ_COMMAND;
|
||||
// Whole package but checksum
|
||||
const uint8_t checksum = sum_bytes(data->raw, sizeof(data->raw) - 1, BL0939_READ_COMMAND) ^ 0xFF;
|
||||
for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) {
|
||||
checksum += data->raw[i];
|
||||
}
|
||||
checksum ^= 0xFF;
|
||||
if (checksum != data->checksum) {
|
||||
ESP_LOGW(TAG, "BL0939 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
|
||||
}
|
||||
@@ -116,24 +120,13 @@ void BL0939::received_package_(const DataPacket *data) const {
|
||||
energy_sensor_sum_->publish_state(total_energy_consumption);
|
||||
}
|
||||
|
||||
if (this->internal_temperature_sensor_ != nullptr) {
|
||||
const uint16_t raw_temperature = ((uint16_t(data->tps1.h) << 8) | data->tps1.l) & 0x3FF;
|
||||
// Datasheet temperature conversion: Tx = (170 / 448) * (TB / 2 - 32) - 45.
|
||||
const float temperature = (170.0f / 448.0f) * (raw_temperature / 2.0f - 32.0f) - 45.0f;
|
||||
this->internal_temperature_sensor_->publish_state(temperature);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG,
|
||||
"BL0939: U %fV, I1 %fA, I2 %fA, P1 %fW, P2 %fW, CntA %" PRId32 ", CntB %" PRId32 ", ∫P1 %fkWh, ∫P2 %fkWh",
|
||||
v_rms, ia_rms, ib_rms, a_watt, b_watt, cfa_cnt, cfb_cnt, a_energy_consumption, b_energy_consumption);
|
||||
}
|
||||
|
||||
void BL0939::dump_config() { // NOLINT(readability-function-cognitive-complexity)
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"BL0939:\n"
|
||||
" Mode: %s",
|
||||
this->mode_ == BL0939Mode::BL0939_MODE_CURRENT_TRANSFORMER ? LOG_STR_LITERAL("current_transformer")
|
||||
: LOG_STR_LITERAL("direct"));
|
||||
ESP_LOGCONFIG(TAG, "BL0939:");
|
||||
LOG_SENSOR("", "Voltage", this->voltage_sensor_);
|
||||
LOG_SENSOR("", "Current 1", this->current_sensor_1_);
|
||||
LOG_SENSOR("", "Current 2", this->current_sensor_2_);
|
||||
@@ -142,7 +135,6 @@ void BL0939::dump_config() { // NOLINT(readability-function-cognitive-complexit
|
||||
LOG_SENSOR("", "Energy 1", this->energy_sensor_1_);
|
||||
LOG_SENSOR("", "Energy 2", this->energy_sensor_2_);
|
||||
LOG_SENSOR("", "Energy sum", this->energy_sensor_sum_);
|
||||
LOG_SENSOR("", "Internal Temperature", this->internal_temperature_sensor_);
|
||||
}
|
||||
|
||||
uint32_t BL0939::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; }
|
||||
|
||||
@@ -8,32 +8,13 @@ namespace esphome::bl0939 {
|
||||
|
||||
// https://datasheet.lcsc.com/lcsc/2108071830_BL-Shanghai-Belling-BL0939_C2841044.pdf
|
||||
// (unfortunately chinese, but the formulas can be easily understood)
|
||||
// The datasheet has one set of formulas for direct mode (current shunt) and one for current transformer mode.
|
||||
|
||||
// Direct mode.
|
||||
// Sonoff Dual R3 V2 has the exact same resistor values for the current shunts (RL=1miliOhm)
|
||||
// and for the voltage divider (R1=0.51kOhm, R2=5*390kOhm)
|
||||
// as in the manufacturer's reference circuit, so the same formulas were used here (Vref=1.218V)
|
||||
static constexpr float BL0939_IREF = 324004 * 1 / 1.218;
|
||||
static constexpr float BL0939_UREF = 79931 * 0.51 * 1000 / (1.218 * (5 * 390 + 0.51));
|
||||
static constexpr float BL0939_PREF = 4046 * 1 * 0.51 * 1000 / (1.218 * 1.218 * (5 * 390 + 0.51));
|
||||
static constexpr float BL0939_EREF = 3.6e6 * 4046 * 1 * 0.51 * 1000 / (1638.4 * 256 * 1.218 * 1.218 * (5 * 390 + 0.51));
|
||||
|
||||
// Current transformer mode.
|
||||
// Values from the Seeed Studio XIAO 2-Channel Wi-Fi AC Energy Meter V1.0 schematic (Vref=1.218V).
|
||||
// Current: 100A:50mA CT (2000:1) into a 0.6Ohm burden (2 * 0.3Ohm), so 0.3mV per primary amp.
|
||||
// That is the same as a 0.3mOhm shunt in the direct mode formula.
|
||||
// Voltage: ZMPT107-1 2mA:2mA voltage transformer with R2=5*22kOhm on the primary and R1=24.9Ohm on
|
||||
// the secondary. The transformer isolates R1 from R2, so the divider is R1/R2 instead of R1/(R1+R2).
|
||||
static constexpr float BL0939_IREF_CT = 324004 * 0.3 / 1.218;
|
||||
static constexpr float BL0939_UREF_CT = 79931 * 24.9 * 1000 / (1.218 * 5 * 22000);
|
||||
static constexpr float BL0939_PREF_CT = 4046 * 0.3 * 24.9 * 1000 / (1.218 * 1.218 * 5 * 22000);
|
||||
static constexpr float BL0939_EREF_CT = 3.6e6 * 4046 * 0.3 * 24.9 * 1000 / (1638.4 * 256 * 1.218 * 1.218 * 5 * 22000);
|
||||
|
||||
enum class BL0939Mode : uint8_t {
|
||||
BL0939_MODE_DIRECT = 0,
|
||||
BL0939_MODE_CURRENT_TRANSFORMER = 1,
|
||||
};
|
||||
static const float BL0939_IREF = 324004 * 1 / 1.218;
|
||||
static const float BL0939_UREF = 79931 * 0.51 * 1000 / (1.218 * (5 * 390 + 0.51));
|
||||
static const float BL0939_PREF = 4046 * 1 * 0.51 * 1000 / (1.218 * 1.218 * (5 * 390 + 0.51));
|
||||
static const float BL0939_EREF = 3.6e6 * 4046 * 1 * 0.51 * 1000 / (1638.4 * 256 * 1.218 * 1.218 * (5 * 390 + 0.51));
|
||||
|
||||
struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align)
|
||||
uint8_t l;
|
||||
@@ -85,16 +66,6 @@ class BL0939 final : public PollingComponent, public uart::UARTDevice {
|
||||
void set_energy_sensor_1(sensor::Sensor *energy_sensor_1) { energy_sensor_1_ = energy_sensor_1; }
|
||||
void set_energy_sensor_2(sensor::Sensor *energy_sensor_2) { energy_sensor_2_ = energy_sensor_2; }
|
||||
void set_energy_sensor_sum(sensor::Sensor *energy_sensor_sum) { energy_sensor_sum_ = energy_sensor_sum; }
|
||||
void set_internal_temperature_sensor(sensor::Sensor *sensor) { this->internal_temperature_sensor_ = sensor; }
|
||||
// Inline so a constant mode from codegen folds down to plain stores.
|
||||
void set_mode(BL0939Mode mode) {
|
||||
this->mode_ = mode;
|
||||
const bool current_transformer = mode == BL0939Mode::BL0939_MODE_CURRENT_TRANSFORMER;
|
||||
this->power_reference_ = current_transformer ? BL0939_PREF_CT : BL0939_PREF;
|
||||
this->voltage_reference_ = current_transformer ? BL0939_UREF_CT : BL0939_UREF;
|
||||
this->current_reference_ = current_transformer ? BL0939_IREF_CT : BL0939_IREF;
|
||||
this->energy_reference_ = current_transformer ? BL0939_EREF_CT : BL0939_EREF;
|
||||
}
|
||||
|
||||
void loop() override;
|
||||
|
||||
@@ -113,7 +84,6 @@ class BL0939 final : public PollingComponent, public uart::UARTDevice {
|
||||
sensor::Sensor *energy_sensor_1_{nullptr};
|
||||
sensor::Sensor *energy_sensor_2_{nullptr};
|
||||
sensor::Sensor *energy_sensor_sum_{nullptr};
|
||||
sensor::Sensor *internal_temperature_sensor_{nullptr};
|
||||
|
||||
// Divide by this to turn into Watt
|
||||
float power_reference_ = BL0939_PREF;
|
||||
@@ -123,7 +93,6 @@ class BL0939 final : public PollingComponent, public uart::UARTDevice {
|
||||
float current_reference_ = BL0939_IREF;
|
||||
// Divide by this to turn into kWh
|
||||
float energy_reference_ = BL0939_EREF;
|
||||
BL0939Mode mode_{BL0939Mode::BL0939_MODE_DIRECT};
|
||||
|
||||
static uint32_t to_uint32_t(ube24_t input);
|
||||
|
||||
|
||||
@@ -3,19 +3,14 @@ from esphome.components import sensor, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INTERNAL_TEMPERATURE,
|
||||
CONF_MODE,
|
||||
CONF_VOLTAGE,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
@@ -34,22 +29,11 @@ CONF_ENERGY_TOTAL = "energy_total"
|
||||
|
||||
bl0939_ns = cg.esphome_ns.namespace("bl0939")
|
||||
BL0939 = bl0939_ns.class_("BL0939", cg.PollingComponent, uart.UARTDevice)
|
||||
BL0939Mode = bl0939_ns.enum("BL0939Mode", is_class=True)
|
||||
MODE_OPTIONS = {
|
||||
"direct": BL0939Mode.BL0939_MODE_DIRECT,
|
||||
"current_transformer": BL0939Mode.BL0939_MODE_CURRENT_TRANSFORMER,
|
||||
}
|
||||
# Must match the mode_ member initializer in bl0939.h; set_mode() is skipped
|
||||
# when the config already equals it.
|
||||
DEFAULT_MODE = "direct"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BL0939),
|
||||
cv.Optional(CONF_MODE, default=DEFAULT_MODE): cv.enum(
|
||||
MODE_OPTIONS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
@@ -98,13 +82,6 @@ CONFIG_SCHEMA = (
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
@@ -117,9 +94,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if (mode := config[CONF_MODE]) != DEFAULT_MODE:
|
||||
cg.add(var.set_mode(mode))
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
await sensors(CONF_CURRENT_1, var.set_current_sensor_1)
|
||||
@@ -129,4 +103,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
await sensors(CONF_ENERGY_1, var.set_energy_sensor_1)
|
||||
await sensors(CONF_ENERGY_2, var.set_energy_sensor_2)
|
||||
await sensors(CONF_ENERGY_TOTAL, var.set_energy_sensor_sum)
|
||||
await sensors(CONF_INTERNAL_TEMPERATURE, var.set_internal_temperature_sensor)
|
||||
|
||||
@@ -44,9 +44,13 @@ void BL0940::loop() {
|
||||
}
|
||||
|
||||
bool BL0940::validate_checksum_(DataPacket *data) {
|
||||
uint8_t checksum = this->read_command_;
|
||||
// Whole package but checksum
|
||||
const uint8_t checksum =
|
||||
sum_bytes(reinterpret_cast<const uint8_t *>(data), sizeof(*data) - 1, this->read_command_) ^ 0xFF;
|
||||
uint8_t *raw = (uint8_t *) data;
|
||||
for (uint32_t i = 0; i < sizeof(*data) - 1; i++) {
|
||||
checksum += raw[i];
|
||||
}
|
||||
checksum ^= 0xFF;
|
||||
if (checksum != data->checksum) {
|
||||
ESP_LOGW(TAG, "Invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
|
||||
}
|
||||
|
||||
@@ -70,10 +70,13 @@ void BL0942::loop() {
|
||||
}
|
||||
|
||||
bool BL0942::validate_checksum_(DataPacket *data) {
|
||||
uint8_t checksum = BL0942_READ_COMMAND | this->address_;
|
||||
// Whole package but checksum
|
||||
const uint8_t checksum =
|
||||
sum_bytes(reinterpret_cast<const uint8_t *>(data), sizeof(*data) - 1, BL0942_READ_COMMAND | this->address_) ^
|
||||
0xFF;
|
||||
uint8_t *raw = (uint8_t *) data;
|
||||
for (uint32_t i = 0; i < sizeof(*data) - 1; i++) {
|
||||
checksum += raw[i];
|
||||
}
|
||||
checksum ^= 0xFF;
|
||||
if (checksum != data->checksum) {
|
||||
ESP_LOGW(TAG, "BL0942 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ from esphome.components.logger import request_log_listener
|
||||
from esphome.components.uart import (
|
||||
UARTComponent,
|
||||
debug_to_code,
|
||||
mark_unclocked,
|
||||
maybe_empty_debug,
|
||||
uart_ns,
|
||||
)
|
||||
@@ -43,7 +42,7 @@ def validate_rx_buffer(config: ConfigType) -> ConfigType:
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.All(cv.declare_id(BLENUS), mark_unclocked),
|
||||
cv.GenerateID(): cv.declare_id(BLENUS),
|
||||
cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of(
|
||||
*[CONF_LOGS, CONF_UART], lower=True
|
||||
),
|
||||
|
||||
@@ -5,16 +5,12 @@
|
||||
|
||||
namespace esphome::bmi270 {
|
||||
|
||||
#ifdef USE_BMI270_BMM150
|
||||
using namespace bmm150; // NOLINT - sole consumer of the BMM150 aux-device definitions
|
||||
#endif
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bmi270");
|
||||
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
static constexpr size_t MAX_I2C_BUFFER_SIZE = 32;
|
||||
static const size_t MAX_I2C_BUFFER_SIZE = 32;
|
||||
#else
|
||||
static constexpr size_t MAX_I2C_BUFFER_SIZE = 256;
|
||||
static const size_t MAX_I2C_BUFFER_SIZE = 256;
|
||||
#endif
|
||||
|
||||
// Configuration blob upload
|
||||
@@ -23,12 +19,12 @@ static constexpr size_t MAX_I2C_BUFFER_SIZE = 256;
|
||||
|
||||
bool BMI270Component::load_config_file_() {
|
||||
// 1. Disable advanced power-save so the config port is accessible
|
||||
if (!this->write_byte(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_ADV_POWER_SAVE_OFF))
|
||||
if (!this->write_byte(BMI270_REG_PWR_CONF, 0x00))
|
||||
return false;
|
||||
delay(1);
|
||||
|
||||
// 2. Prepare config load: write 0x00 to INIT_CTRL to start
|
||||
if (!this->write_byte(BMI270_REG_INIT_CTRL, BMI270_INIT_CTRL_LOAD_START))
|
||||
if (!this->write_byte(BMI270_REG_INIT_CTRL, 0x00))
|
||||
return false;
|
||||
|
||||
// 3. Burst-write the config in pages
|
||||
@@ -54,7 +50,7 @@ bool BMI270Component::load_config_file_() {
|
||||
}
|
||||
|
||||
// 4. Signal end of config load
|
||||
if (!this->write_byte(BMI270_REG_INIT_CTRL, BMI270_INIT_CTRL_LOAD_DONE))
|
||||
if (!this->write_byte(BMI270_REG_INIT_CTRL, 0x01))
|
||||
return false;
|
||||
delay(20); // spec: wait ≥20 ms for init to complete
|
||||
|
||||
@@ -62,7 +58,7 @@ bool BMI270Component::load_config_file_() {
|
||||
uint8_t status = 0;
|
||||
if (!this->read_byte(BMI270_REG_INTERNAL_STATUS, &status))
|
||||
return false;
|
||||
if ((status & BMI270_INTERNAL_STATUS_MASK) != BMI270_INTERNAL_STATUS_INIT_OK) {
|
||||
if ((status & 0x0F) != 0x01) {
|
||||
ESP_LOGE(TAG, "Config load failed: INTERNAL_STATUS=0x%02X (expected 0x01)", status);
|
||||
return false;
|
||||
}
|
||||
@@ -88,7 +84,7 @@ void BMI270Component::setup() {
|
||||
ESP_LOGD(TAG, "Chip ID: 0x%02X", chip_id);
|
||||
|
||||
// 2. Soft-reset via CMD register (0x7E = 0xB6)
|
||||
if (!this->write_byte(BMI270_REG_CMD, BMI270_CMD_SOFT_RESET)) {
|
||||
if (!this->write_byte(0x7E, 0xB6)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -104,8 +100,7 @@ void BMI270Component::setup() {
|
||||
|
||||
// 5. Configure accelerometer
|
||||
// ACC_CONF: ODR | BWP(0x2 = normal avg4) | perf_mode(1)
|
||||
uint8_t acc_conf =
|
||||
(uint8_t) (accel_odr_) | (BMI270_CONF_BWP_NORMAL << BMI270_CONF_BWP_SHIFT) | BMI270_ACC_CONF_PERF_MODE;
|
||||
uint8_t acc_conf = (uint8_t) (accel_odr_) | (0x2 << 4) | (1 << 7);
|
||||
if (!this->write_byte(BMI270_REG_ACC_CONF, acc_conf)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
@@ -117,8 +112,7 @@ void BMI270Component::setup() {
|
||||
|
||||
// 6. Configure gyroscope
|
||||
// GYR_CONF: ODR | BWP(0x2 = normal) | noise_perf(1) | filter_perf(1)
|
||||
uint8_t gyr_conf = (uint8_t) (gyro_odr_) | (BMI270_CONF_BWP_NORMAL << BMI270_CONF_BWP_SHIFT) |
|
||||
BMI270_GYR_CONF_NOISE_PERF | BMI270_GYR_CONF_FILTER_PERF;
|
||||
uint8_t gyr_conf = (uint8_t) (gyro_odr_) | (0x2 << 4) | (1 << 6) | (1 << 7);
|
||||
if (!this->write_byte(BMI270_REG_GYR_CONF, gyr_conf)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
@@ -130,7 +124,7 @@ void BMI270Component::setup() {
|
||||
|
||||
// 7. Enable accelerometer, gyroscope, and temperature sensor
|
||||
// PWR_CTRL bits: temp_en[3] | gyr_en[2] | acc_en[1]
|
||||
if (!this->write_byte(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_IMU)) {
|
||||
if (!this->write_byte(BMI270_REG_PWR_CTRL, 0x0E)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -138,133 +132,14 @@ void BMI270Component::setup() {
|
||||
|
||||
// 8. Re-enable advanced power save (optional; keeps current low between reads)
|
||||
// Disabled here for simplicity – leave in performance mode
|
||||
if (!this->write_byte(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_FIFO_SELF_WAKEUP)) { // bit1 = fifo_self_wakeup
|
||||
if (!this->write_byte(BMI270_REG_PWR_CONF, 0x02)) { // bit1 = fifo_self_wakeup
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// 9. Optionally bring up whatever is wired to the aux interface.
|
||||
// This is entirely additive: it is off by default, and failure only disables
|
||||
// magnetometer reporting rather than the whole component.
|
||||
#ifdef USE_BMI270_BMM150
|
||||
if (this->aux_device_ == BMI270_AUX_DEVICE_BMM150) {
|
||||
this->magnetometer_ready_ = this->setup_magnetometer_();
|
||||
if (!this->magnetometer_ready_) {
|
||||
ESP_LOGW(TAG, "BMM150 magnetometer setup failed; continuing without it");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGCONFIG(TAG, "BMI270 initialised successfully");
|
||||
}
|
||||
|
||||
#ifdef USE_BMI270_BMM150
|
||||
bool BMI270Component::wait_for_aux_idle_(uint32_t timeout_ms) {
|
||||
// The STATUS aux-busy bit is set while an aux transaction is in flight. Aux transaction
|
||||
// duration depends on the attached device's own timing, so poll with a bounded
|
||||
// timeout instead of guessing a fixed delay.
|
||||
uint32_t start = millis();
|
||||
while (true) {
|
||||
uint8_t status = 0;
|
||||
if (!this->read_byte(BMI270_REG_STATUS, &status))
|
||||
return false;
|
||||
if ((status & BMI270_STATUS_AUX_BUSY) == 0)
|
||||
return true;
|
||||
if (millis() - start >= timeout_ms)
|
||||
return false;
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
bool BMI270Component::write_aux_register_(uint8_t reg, uint8_t value) {
|
||||
if (!this->write_byte(BMI270_REG_AUX_WR_DATA, value))
|
||||
return false;
|
||||
if (!this->write_byte(BMI270_REG_AUX_WR_ADDR, reg))
|
||||
return false;
|
||||
return this->wait_for_aux_idle_(20);
|
||||
}
|
||||
|
||||
bool BMI270Component::read_aux_block_(uint8_t reg, uint8_t *buf) {
|
||||
if (!this->write_byte(BMI270_REG_AUX_RD_ADDR, reg))
|
||||
return false;
|
||||
if (!this->wait_for_aux_idle_(20))
|
||||
return false;
|
||||
return this->read_bytes(BMI270_REG_AUX_X_LSB, buf, BMI270_AUX_DATA_LEN);
|
||||
}
|
||||
|
||||
bool BMI270Component::read_magnetometer_trim_() {
|
||||
uint8_t regs[BMM150_TRIM_BLOCKS * BMM150_TRIM_BLOCK_LEN];
|
||||
for (uint8_t i = 0; i < BMM150_TRIM_BLOCKS; i++) {
|
||||
if (!this->read_aux_block_(BMM150_REG_TRIM_START + i * BMM150_TRIM_BLOCK_LEN, regs + i * BMM150_TRIM_BLOCK_LEN))
|
||||
return false;
|
||||
}
|
||||
this->mag_trim_ = bmm150_parse_trim(regs);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BMI270Component::setup_magnetometer_() {
|
||||
// Sequence adapted from the Bosch BMI270 aux-interface protocol: enable the secondary
|
||||
// I2C master in manual mode, point it at the BMM150's address, power the BMM150 up
|
||||
// and soft-reset it, verify its chip ID, read its factory trim values, put it in
|
||||
// normal/30Hz mode, then switch the aux interface to auto-burst mode so BMI270 keeps
|
||||
// AUX_X/Y/Z/R refreshed from the BMM150's data registers.
|
||||
ESP_LOGD(TAG, "Setting up BMM150 magnetometer via BMI270 aux interface...");
|
||||
|
||||
if (!this->write_byte(BMI270_REG_IF_CONF, BMI270_IF_CONF_AUX_IF_EN))
|
||||
return false;
|
||||
if (!this->write_byte(BMI270_REG_AUX_IF_CONF, BMI270_AUX_IF_CONF_MANUAL_EN | BMI270_AUX_IF_CONF_MAN_RD_BURST_8))
|
||||
return false;
|
||||
if (!this->write_byte(BMI270_REG_AUX_DEV_ID, (uint8_t) (this->aux_device_address_ << 1)))
|
||||
return false;
|
||||
|
||||
// Suspend -> sleep, then a soft reset so the BMM150 starts from a known state even if it
|
||||
// stayed powered across a restart. The chip ID is only readable once the power bit is set.
|
||||
if (!this->write_aux_register_(BMM150_REG_POWER_CONTROL, BMM150_CMD_POWER_ON)) {
|
||||
ESP_LOGW(TAG, "BMM150 power-on write timed out");
|
||||
return false;
|
||||
}
|
||||
delay(BMM150_START_UP_TIME_MS);
|
||||
if (!this->write_aux_register_(BMM150_REG_POWER_CONTROL, BMM150_CMD_SOFT_RESET)) {
|
||||
ESP_LOGW(TAG, "BMM150 soft-reset write timed out");
|
||||
return false;
|
||||
}
|
||||
delay(BMM150_SOFT_RESET_TIME_MS);
|
||||
|
||||
uint8_t chip_id[BMI270_AUX_DATA_LEN];
|
||||
if (!this->read_aux_block_(BMM150_REG_CHIP_ID, chip_id)) {
|
||||
ESP_LOGW(TAG, "BMM150 chip-ID read failed");
|
||||
return false;
|
||||
}
|
||||
if (chip_id[0] != BMM150_CHIP_ID_VALUE) {
|
||||
ESP_LOGW(TAG, "BMM150 not detected (chip ID 0x%02X, expected 0x%02X)", chip_id[0], BMM150_CHIP_ID_VALUE);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->read_magnetometer_trim_()) {
|
||||
ESP_LOGW(TAG, "BMM150 trim data read failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->write_aux_register_(BMM150_REG_OP_MODE, BMM150_CMD_NORMAL_MODE_ODR_30HZ)) {
|
||||
ESP_LOGW(TAG, "BMM150 mode write timed out");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Switch to auto-burst mode: BMI270 will keep polling 8 bytes from the BMM150's
|
||||
// data registers (0x42) into its own AUX_X/Y/Z/R registers automatically.
|
||||
if (!this->write_byte(BMI270_REG_AUX_IF_CONF, BMI270_AUX_IF_CONF_FCU_WRITE_EN | BMI270_AUX_IF_CONF_MAN_RD_BURST_8 |
|
||||
BMI270_AUX_IF_CONF_RD_BURST_8))
|
||||
return false;
|
||||
if (!this->write_byte(BMI270_REG_AUX_RD_ADDR, BMM150_REG_DATA_X_LSB))
|
||||
return false;
|
||||
if (!this->write_byte(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_IMU | BMI270_PWR_CTRL_AUX_EN))
|
||||
return false;
|
||||
|
||||
ESP_LOGD(TAG, "BMM150 magnetometer ready");
|
||||
return true;
|
||||
}
|
||||
#endif // USE_BMI270_BMM150
|
||||
|
||||
void BMI270Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BMI270 IMU:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
@@ -278,12 +153,6 @@ void BMI270Component::dump_config() {
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[accel_range_]);
|
||||
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[gyro_range_]);
|
||||
#ifdef USE_BMI270_BMM150
|
||||
if (this->aux_device_ == BMI270_AUX_DEVICE_BMM150) {
|
||||
ESP_LOGCONFIG(TAG, " Aux device : BMM150 magnetometer (%s)",
|
||||
this->magnetometer_ready_ ? LOG_STR_LITERAL("ready") : LOG_STR_LITERAL("NOT DETECTED"));
|
||||
}
|
||||
#endif
|
||||
MotionComponent::dump_config();
|
||||
}
|
||||
|
||||
@@ -294,22 +163,9 @@ bool BMI270Component::update_data(motion::MotionData &data) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
// Accelerometer: registers 0x0C–0x11 (6 bytes: x_lsb, x_msb, y_lsb, y_msb, z_lsb, z_msb)
|
||||
// When the magnetometer is active, the 8 aux data bytes (0x04–0x0B) sit directly before
|
||||
// the accel data, so one burst read from AUX_X_LSB fetches both.
|
||||
#ifdef USE_BMI270_BMM150
|
||||
const bool read_mag = this->magnetometer_ready_ && !this->magnetometer_callback_.empty();
|
||||
uint8_t buf[BMI270_AUX_DATA_LEN + REG_READ_LEN];
|
||||
const uint8_t *mag_raw = buf;
|
||||
const uint8_t *raw_data = read_mag ? buf + BMI270_AUX_DATA_LEN : buf;
|
||||
const bool read_ok = read_mag ? this->read_bytes(BMI270_REG_AUX_X_LSB, buf, sizeof(buf))
|
||||
: this->read_bytes(BMI270_REG_DATA_8, buf, REG_READ_LEN);
|
||||
#else
|
||||
uint8_t buf[REG_READ_LEN];
|
||||
const uint8_t *raw_data = buf;
|
||||
const bool read_ok = this->read_bytes(BMI270_REG_DATA_8, buf, REG_READ_LEN);
|
||||
#endif
|
||||
if (!read_ok) {
|
||||
// Accelerometer: registers 0x0C–0x11 (6 bytes: x_lsb, x_msb, y_lsb, y_msb, z_lsb, z_msb)
|
||||
uint8_t raw_data[REG_READ_LEN];
|
||||
if (!this->read_bytes(BMI270_REG_DATA_8, raw_data, REG_READ_LEN)) {
|
||||
ESP_LOGW(TAG, "Failed to read IMU data");
|
||||
return false;
|
||||
}
|
||||
@@ -339,21 +195,14 @@ bool BMI270Component::update_data(motion::MotionData &data) {
|
||||
data.angular_rate[motion::Y_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 3] << 8) | raw_data[GYR_OFFS + 2]) * scale;
|
||||
data.angular_rate[motion::Z_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 5] << 8) | raw_data[GYR_OFFS + 4]) * scale;
|
||||
|
||||
if (!this->temperature_callback_.empty()) {
|
||||
// Temperature: registers 0x22–0x23
|
||||
// Formula from datasheet: T[°C] = raw / 512 + 23
|
||||
static constexpr uint8_t TEMP_OFFS = BMI270_REG_TEMP_0 - BMI270_REG_DATA_8;
|
||||
int16_t raw_t = (int16_t) ((raw_data[TEMP_OFFS + 1] << 8) | raw_data[TEMP_OFFS + 0]);
|
||||
float temperature = (raw_t / 512.0f) + 23.0f;
|
||||
this->temperature_callback_.call(temperature);
|
||||
}
|
||||
|
||||
#ifdef USE_BMI270_BMM150
|
||||
if (read_mag) {
|
||||
const BMM150Data mag_data = bmm150_convert(this->mag_trim_, mag_raw);
|
||||
this->magnetometer_callback_.call(mag_data);
|
||||
}
|
||||
#endif
|
||||
if (this->temperature_callback_.empty())
|
||||
return true;
|
||||
// Temperature: registers 0x22–0x23
|
||||
// Formula from datasheet: T[°C] = raw / 512 + 23
|
||||
static constexpr uint8_t TEMP_OFFS = BMI270_REG_TEMP_0 - BMI270_REG_DATA_8;
|
||||
int16_t raw_t = (int16_t) ((raw_data[TEMP_OFFS + 1] << 8) | raw_data[TEMP_OFFS + 0]);
|
||||
float temperature = (raw_t / 512.0f) + 23.0f;
|
||||
this->temperature_callback_.call(temperature);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,81 +4,35 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BMI270_BMM150
|
||||
#include "bmm150_aux.h"
|
||||
#endif
|
||||
#include <functional>
|
||||
|
||||
namespace esphome::bmi270 {
|
||||
|
||||
// Register map
|
||||
static constexpr uint8_t BMI270_REG_CHIP_ID = 0x00;
|
||||
static constexpr uint8_t BMI270_REG_ERR_REG = 0x02;
|
||||
static constexpr uint8_t BMI270_REG_STATUS = 0x03;
|
||||
static constexpr uint8_t BMI270_REG_DATA_8 = 0x0C; // ACC_X LSB
|
||||
static constexpr uint8_t BMI270_REG_DATA_14 = 0x12; // GYR_X LSB
|
||||
static constexpr uint8_t BMI270_REG_TEMP_0 = 0x22;
|
||||
static constexpr uint8_t BMI270_REG_TEMP_MSB = 0x23; // temperature (2 bytes big-endian ish)
|
||||
static const uint8_t BMI270_REG_CHIP_ID = 0x00;
|
||||
static const uint8_t BMI270_REG_ERR_REG = 0x02;
|
||||
static const uint8_t BMI270_REG_STATUS = 0x03;
|
||||
static const uint8_t BMI270_REG_DATA_8 = 0x0C; // ACC_X LSB
|
||||
static const uint8_t BMI270_REG_DATA_14 = 0x12; // GYR_X LSB
|
||||
static const uint8_t BMI270_REG_TEMP_0 = 0x22;
|
||||
static const uint8_t BMI270_REG_TEMP_MSB = 0x23; // temperature (2 bytes big-endian ish)
|
||||
|
||||
static constexpr uint8_t REG_READ_LEN =
|
||||
BMI270_REG_TEMP_MSB - BMI270_REG_DATA_8 +
|
||||
1; // 0x23 - 0x0C + 1 = 0x18 bytes total for accel(6) + gyro(6) + temp(2) + padding(4)
|
||||
|
||||
static constexpr uint8_t BMI270_REG_PWR_CONF = 0x7C;
|
||||
static constexpr uint8_t BMI270_REG_PWR_CTRL = 0x7D;
|
||||
static constexpr uint8_t BMI270_REG_INIT_CTRL = 0x59;
|
||||
static constexpr uint8_t BMI270_REG_INIT_DATA = 0x5E;
|
||||
static constexpr uint8_t BMI270_REG_INIT_ADDR_0 = 0x5B;
|
||||
static constexpr uint8_t BMI270_REG_INTERNAL_STATUS = 0x21;
|
||||
static constexpr uint8_t BMI270_REG_ACC_CONF = 0x40;
|
||||
static constexpr uint8_t BMI270_REG_ACC_RANGE = 0x41;
|
||||
static constexpr uint8_t BMI270_REG_GYR_CONF = 0x42;
|
||||
static constexpr uint8_t BMI270_REG_GYR_RANGE = 0x43;
|
||||
static const uint8_t BMI270_REG_PWR_CONF = 0x7C;
|
||||
static const uint8_t BMI270_REG_PWR_CTRL = 0x7D;
|
||||
static const uint8_t BMI270_REG_INIT_CTRL = 0x59;
|
||||
static const uint8_t BMI270_REG_INIT_DATA = 0x5E;
|
||||
static const uint8_t BMI270_REG_INIT_ADDR_0 = 0x5B;
|
||||
static const uint8_t BMI270_REG_INTERNAL_STATUS = 0x21;
|
||||
static const uint8_t BMI270_REG_ACC_CONF = 0x40;
|
||||
static const uint8_t BMI270_REG_ACC_RANGE = 0x41;
|
||||
static const uint8_t BMI270_REG_GYR_CONF = 0x42;
|
||||
static const uint8_t BMI270_REG_GYR_RANGE = 0x43;
|
||||
|
||||
static constexpr uint8_t BMI270_CHIP_ID_VALUE = 0x24;
|
||||
|
||||
// Auxiliary (secondary I2C master) interface registers.
|
||||
// Used to optionally drive a BMM150 magnetometer wired to the BMI270's AUX pins
|
||||
// instead of the main I2C bus - the BMM150 has no address of its own on that bus.
|
||||
static constexpr uint8_t BMI270_REG_AUX_X_LSB = 0x04; // start of aux data: X,Y,Z,R (8 bytes)
|
||||
static constexpr uint8_t BMI270_REG_AUX_DEV_ID = 0x4B;
|
||||
static constexpr uint8_t BMI270_REG_AUX_IF_CONF = 0x4C;
|
||||
static constexpr uint8_t BMI270_REG_AUX_RD_ADDR = 0x4D;
|
||||
static constexpr uint8_t BMI270_REG_AUX_WR_ADDR = 0x4E;
|
||||
static constexpr uint8_t BMI270_REG_AUX_WR_DATA = 0x4F;
|
||||
static constexpr uint8_t BMI270_REG_IF_CONF = 0x6B;
|
||||
static constexpr uint8_t BMI270_REG_CMD = 0x7E;
|
||||
|
||||
// Register values and bitfields
|
||||
static constexpr uint8_t BMI270_CMD_SOFT_RESET = 0xB6;
|
||||
static constexpr uint8_t BMI270_PWR_CONF_ADV_POWER_SAVE_OFF = 0x00;
|
||||
static constexpr uint8_t BMI270_PWR_CONF_FIFO_SELF_WAKEUP = 0x02;
|
||||
static constexpr uint8_t BMI270_INIT_CTRL_LOAD_START = 0x00;
|
||||
static constexpr uint8_t BMI270_INIT_CTRL_LOAD_DONE = 0x01;
|
||||
static constexpr uint8_t BMI270_INTERNAL_STATUS_MASK = 0x0F;
|
||||
static constexpr uint8_t BMI270_INTERNAL_STATUS_INIT_OK = 0x01;
|
||||
static constexpr uint8_t BMI270_CONF_BWP_NORMAL = 0x02; // filter bandwidth, bits 5:4
|
||||
static constexpr uint8_t BMI270_CONF_BWP_SHIFT = 4;
|
||||
static constexpr uint8_t BMI270_ACC_CONF_PERF_MODE = 0x80;
|
||||
static constexpr uint8_t BMI270_GYR_CONF_NOISE_PERF = 0x40;
|
||||
static constexpr uint8_t BMI270_GYR_CONF_FILTER_PERF = 0x80;
|
||||
static constexpr uint8_t BMI270_STATUS_AUX_BUSY = 0x04;
|
||||
static constexpr uint8_t BMI270_IF_CONF_AUX_IF_EN = 0x20;
|
||||
static constexpr uint8_t BMI270_PWR_CTRL_AUX_EN = 0x01;
|
||||
static constexpr uint8_t BMI270_PWR_CTRL_ACC_EN = 0x02;
|
||||
static constexpr uint8_t BMI270_PWR_CTRL_GYR_EN = 0x04;
|
||||
static constexpr uint8_t BMI270_PWR_CTRL_TEMP_EN = 0x08;
|
||||
static constexpr uint8_t BMI270_PWR_CTRL_IMU =
|
||||
BMI270_PWR_CTRL_ACC_EN | BMI270_PWR_CTRL_GYR_EN | BMI270_PWR_CTRL_TEMP_EN;
|
||||
static constexpr uint8_t BMI270_AUX_IF_CONF_MANUAL_EN = 0x80;
|
||||
static constexpr uint8_t BMI270_AUX_IF_CONF_FCU_WRITE_EN = 0x40;
|
||||
static constexpr uint8_t BMI270_AUX_IF_CONF_MAN_RD_BURST_8 = 0x0C; // bits 3:2 = 3
|
||||
static constexpr uint8_t BMI270_AUX_IF_CONF_RD_BURST_8 = 0x03; // bits 1:0 = 3
|
||||
|
||||
// AUX_X_LSB..AUX_R_MSB (8 bytes) sit directly before the accel data, so one burst can read both.
|
||||
static constexpr uint8_t BMI270_AUX_DATA_LEN = 8;
|
||||
static_assert(BMI270_REG_AUX_X_LSB + BMI270_AUX_DATA_LEN == BMI270_REG_DATA_8, "aux data must precede accel data");
|
||||
static const uint8_t BMI270_CHIP_ID_VALUE = 0x24;
|
||||
|
||||
// Accelerometer range options
|
||||
enum BMI270AccelRange : uint8_t {
|
||||
@@ -121,14 +75,7 @@ enum BMI270GyroODR : uint8_t {
|
||||
BMI270_GYRO_ODR_3200 = 0x0D,
|
||||
};
|
||||
|
||||
#ifdef USE_BMI270_BMM150
|
||||
// What is wired to this instance's auxiliary (secondary I2C master) interface; another
|
||||
// instance in the same build may have nothing attached.
|
||||
enum BMI270AuxDevice : uint8_t {
|
||||
BMI270_AUX_DEVICE_NONE = 0,
|
||||
BMI270_AUX_DEVICE_BMM150 = 1,
|
||||
};
|
||||
#endif
|
||||
// ---Data class
|
||||
|
||||
// Main component class
|
||||
class BMI270Component final : public motion::MotionComponent, public i2c::I2CDevice {
|
||||
@@ -144,43 +91,18 @@ class BMI270Component final : public motion::MotionComponent, public i2c::I2CDev
|
||||
void set_gyro_range(BMI270GyroRange r) { this->gyro_range_ = r; }
|
||||
void set_gyro_odr(BMI270GyroODR o) { this->gyro_odr_ = o; }
|
||||
template<typename F> void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward<F>(cb)); }
|
||||
#ifdef USE_BMI270_BMM150
|
||||
void set_aux_device(BMI270AuxDevice device) { this->aux_device_ = device; }
|
||||
void set_aux_device_address(uint8_t address) { this->aux_device_address_ = address; }
|
||||
template<typename F> void add_magnetometer_listener(F &&cb) { this->magnetometer_callback_.add(std::forward<F>(cb)); }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool update_data(motion::MotionData &data) override;
|
||||
bool load_config_file_();
|
||||
#ifdef USE_BMI270_BMM150
|
||||
// Brings up the BMM150 over the BMI270's aux interface. Optional: on failure this only
|
||||
// disables magnetometer reporting, it does not fail the whole component (accel/gyro still work).
|
||||
bool setup_magnetometer_();
|
||||
// Blocking poll of the aux-transaction-busy bit; aux transaction time depends on the
|
||||
// attached device, so this is bounded by retries/timeout rather than a fixed delay.
|
||||
bool wait_for_aux_idle_(uint32_t timeout_ms);
|
||||
// Manual-mode aux read of 8 bytes starting at `reg` on the aux device.
|
||||
bool read_aux_block_(uint8_t reg, uint8_t *buf);
|
||||
// Manual-mode aux write of one byte to `reg` on the aux device.
|
||||
bool write_aux_register_(uint8_t reg, uint8_t value);
|
||||
// Reads the BMM150 factory trim values used to compensate its samples.
|
||||
bool read_magnetometer_trim_();
|
||||
#endif
|
||||
|
||||
// Config
|
||||
BMI270AccelRange accel_range_{BMI270_ACCEL_RANGE_4G};
|
||||
BMI270AccelODR accel_odr_{BMI270_ACCEL_ODR_100};
|
||||
BMI270GyroRange gyro_range_{BMI270_GYRO_RANGE_2000};
|
||||
BMI270GyroODR gyro_odr_{BMI270_GYRO_ODR_200};
|
||||
|
||||
LazyCallbackManager<void(float)> temperature_callback_{};
|
||||
#ifdef USE_BMI270_BMM150
|
||||
LazyCallbackManager<void(const bmm150::BMM150Data &)> magnetometer_callback_{};
|
||||
bmm150::BMM150Trim mag_trim_{};
|
||||
BMI270AuxDevice aux_device_{BMI270_AUX_DEVICE_NONE};
|
||||
uint8_t aux_device_address_{bmm150::BMM150_DEFAULT_I2C_ADDRESS};
|
||||
bool magnetometer_ready_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::bmi270
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
#pragma once
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
// BMM150 magnetometer definitions - the chip that can optionally be wired to the BMI270's
|
||||
// auxiliary (secondary I2C master) interface. Lives in its own header/namespace because this
|
||||
// is BMM150-specific driver knowledge, not BMI270 knowledge - it just can't be a standalone
|
||||
// ESPHome component, since the BMM150 has no address of its own on the main I2C bus and is only
|
||||
// reachable by having the BMI270 proxy register reads/writes through its aux interface.
|
||||
namespace esphome::bmi270::bmm150 {
|
||||
|
||||
static constexpr uint8_t BMM150_DEFAULT_I2C_ADDRESS = 0x10;
|
||||
static constexpr uint8_t BMM150_REG_CHIP_ID = 0x40;
|
||||
static constexpr uint8_t BMM150_REG_DATA_X_LSB = 0x42;
|
||||
static constexpr uint8_t BMM150_REG_POWER_CONTROL = 0x4B;
|
||||
static constexpr uint8_t BMM150_REG_OP_MODE = 0x4C;
|
||||
static constexpr uint8_t BMM150_CHIP_ID_VALUE = 0x32;
|
||||
static constexpr uint8_t BMM150_CMD_POWER_ON = 0x01; // power control bit: suspend -> sleep (3 ms start-up)
|
||||
static constexpr uint8_t BMM150_CMD_SOFT_RESET = 0x83; // soft reset bits 7 and 1, power control bit kept set
|
||||
static constexpr uint8_t BMM150_CMD_NORMAL_MODE_ODR_30HZ = 0x38; // data rate 111b (30 Hz), opmode 00b (normal)
|
||||
static constexpr uint32_t BMM150_START_UP_TIME_MS = 3;
|
||||
static constexpr uint32_t BMM150_SOFT_RESET_TIME_MS = 1;
|
||||
|
||||
// Factory trim registers 0x5D-0x71, read once at setup and used to compensate every sample.
|
||||
static constexpr uint8_t BMM150_REG_TRIM_START = 0x5D;
|
||||
static constexpr uint8_t BMM150_TRIM_BLOCK_LEN = 8; // one aux burst
|
||||
static constexpr uint8_t BMM150_TRIM_BLOCKS = 3; // 0x5D, 0x65, 0x6D -> covers 0x5D-0x74
|
||||
static constexpr uint8_t BMM150_TRIM_X1 = 0x5D;
|
||||
static constexpr uint8_t BMM150_TRIM_Y1 = 0x5E;
|
||||
static constexpr uint8_t BMM150_TRIM_Z4 = 0x62; // 16-bit, LSB first
|
||||
static constexpr uint8_t BMM150_TRIM_X2 = 0x64;
|
||||
static constexpr uint8_t BMM150_TRIM_Y2 = 0x65;
|
||||
static constexpr uint8_t BMM150_TRIM_Z2 = 0x68; // 16-bit
|
||||
static constexpr uint8_t BMM150_TRIM_Z1 = 0x6A; // 16-bit
|
||||
static constexpr uint8_t BMM150_TRIM_XYZ1 = 0x6C; // 16-bit, 15 valid bits
|
||||
static constexpr uint8_t BMM150_TRIM_Z3 = 0x6E; // 16-bit
|
||||
static constexpr uint8_t BMM150_TRIM_XY2 = 0x70;
|
||||
static constexpr uint8_t BMM150_TRIM_XY1 = 0x71;
|
||||
static constexpr uint16_t BMM150_TRIM_XYZ1_MASK = 0x7FFF;
|
||||
|
||||
// Raw ADC values that mean "overflow" per the datasheet.
|
||||
static constexpr int16_t BMM150_XY_OVERFLOW_ADCVAL = -4096;
|
||||
static constexpr int16_t BMM150_Z_OVERFLOW_ADCVAL = -16384;
|
||||
|
||||
// Result of a BMM150 magnetometer reading, in µT. An axis is NaN when the sensor reported overflow.
|
||||
struct BMM150Data {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
};
|
||||
|
||||
// Factory trim values (BMM150 datasheet, compensation section).
|
||||
struct BMM150Trim {
|
||||
int8_t x1, y1, x2, y2, xy2;
|
||||
uint8_t xy1;
|
||||
int16_t z2, z3, z4;
|
||||
uint16_t z1, xyz1;
|
||||
};
|
||||
|
||||
// A little-endian 16-bit trim word, addressed by its register
|
||||
inline uint16_t bmm150_trim_word(const uint8_t *regs, uint8_t reg) {
|
||||
const uint8_t offset = reg - BMM150_REG_TRIM_START;
|
||||
return encode_uint16(regs[offset + 1], regs[offset]);
|
||||
}
|
||||
|
||||
// Decodes the trim registers; `regs` holds the bytes of registers 0x5D onwards (at least 21).
|
||||
inline BMM150Trim bmm150_parse_trim(const uint8_t *regs) {
|
||||
BMM150Trim trim;
|
||||
trim.x1 = static_cast<int8_t>(regs[BMM150_TRIM_X1 - BMM150_REG_TRIM_START]);
|
||||
trim.y1 = static_cast<int8_t>(regs[BMM150_TRIM_Y1 - BMM150_REG_TRIM_START]);
|
||||
trim.z4 = static_cast<int16_t>(bmm150_trim_word(regs, BMM150_TRIM_Z4));
|
||||
trim.x2 = static_cast<int8_t>(regs[BMM150_TRIM_X2 - BMM150_REG_TRIM_START]);
|
||||
trim.y2 = static_cast<int8_t>(regs[BMM150_TRIM_Y2 - BMM150_REG_TRIM_START]);
|
||||
trim.z2 = static_cast<int16_t>(bmm150_trim_word(regs, BMM150_TRIM_Z2));
|
||||
trim.z1 = bmm150_trim_word(regs, BMM150_TRIM_Z1);
|
||||
trim.xyz1 = bmm150_trim_word(regs, BMM150_TRIM_XYZ1) & BMM150_TRIM_XYZ1_MASK;
|
||||
trim.z3 = static_cast<int16_t>(bmm150_trim_word(regs, BMM150_TRIM_Z3));
|
||||
trim.xy2 = static_cast<int8_t>(regs[BMM150_TRIM_XY2 - BMM150_REG_TRIM_START]);
|
||||
trim.xy1 = regs[BMM150_TRIM_XY1 - BMM150_REG_TRIM_START];
|
||||
return trim;
|
||||
}
|
||||
|
||||
// Compensates an X or Y sample; returns µT, or NaN on overflow. Port of the Bosch BMM150 API (float variant).
|
||||
inline float bmm150_compensate_xy(const BMM150Trim &trim, int8_t trim_a, int8_t trim_b, int16_t raw, uint16_t rhall) {
|
||||
if (raw == BMM150_XY_OVERFLOW_ADCVAL || rhall == 0 || trim.xyz1 == 0)
|
||||
return NAN;
|
||||
float retval = (float) trim.xyz1 * 16384.0f / (float) rhall - 16384.0f;
|
||||
float comp1 = (float) trim.xy2 * (retval * retval / 268435456.0f);
|
||||
float comp2 = comp1 + retval * (float) trim.xy1 / 16384.0f;
|
||||
float comp3 = (float) trim_b + 160.0f;
|
||||
float comp4 = (float) raw * ((comp2 + 256.0f) * comp3);
|
||||
return ((comp4 / 8192.0f) + (float) trim_a * 8.0f) / 16.0f;
|
||||
}
|
||||
|
||||
// Compensates a Z sample; returns µT, or NaN on overflow. Port of the Bosch BMM150 API (float variant).
|
||||
inline float bmm150_compensate_z(const BMM150Trim &trim, int16_t raw, uint16_t rhall) {
|
||||
if (raw == BMM150_Z_OVERFLOW_ADCVAL || trim.z2 == 0 || trim.z1 == 0 || trim.xyz1 == 0 || rhall == 0)
|
||||
return NAN;
|
||||
float comp0 = (float) raw - (float) trim.z4;
|
||||
float comp1 = (float) rhall - (float) trim.xyz1;
|
||||
float comp2 = (float) trim.z3 * comp1;
|
||||
float comp3 = (float) trim.z1 * (float) rhall / 32768.0f;
|
||||
float comp4 = (float) trim.z2 + comp3;
|
||||
float comp5 = comp0 * 131072.0f - comp2;
|
||||
return (comp5 / (comp4 * 4.0f)) / 16.0f;
|
||||
}
|
||||
|
||||
// Converts 8 raw bytes from the BMM150 data registers (X, Y, Z, RHALL; 0x42-0x49) to compensated µT.
|
||||
inline BMM150Data bmm150_convert(const BMM150Trim &trim, const uint8_t *raw) {
|
||||
// X/Y are 13-bit (LSB byte bits 7:3), Z is 15-bit (bits 7:1), RHALL is 14-bit (bits 7:2).
|
||||
int16_t x = (int16_t) (((int16_t) (int8_t) raw[1] * 32) | (raw[0] >> 3));
|
||||
int16_t y = (int16_t) (((int16_t) (int8_t) raw[3] * 32) | (raw[2] >> 3));
|
||||
int16_t z = (int16_t) (((int16_t) (int8_t) raw[5] * 128) | (raw[4] >> 1));
|
||||
uint16_t rhall = (uint16_t) (((uint16_t) raw[7] << 6) | (raw[6] >> 2));
|
||||
return BMM150Data{
|
||||
.x = bmm150_compensate_xy(trim, trim.x1, trim.x2, x, rhall),
|
||||
.y = bmm150_compensate_xy(trim, trim.y1, trim.y2, y, rhall),
|
||||
.z = bmm150_compensate_z(trim, z, rhall),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace esphome::bmi270::bmm150
|
||||
@@ -14,11 +14,7 @@ from . import BMI270Component, bmi270_ns
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
CONF_AUX_ADDRESS = "aux_address"
|
||||
CONF_AUX_DEVICE = "aux_device"
|
||||
|
||||
# Enum proxies (must match the C++ enum values exactly)
|
||||
BMI270AuxDevice = bmi270_ns.enum("BMI270AuxDevice")
|
||||
BMI270AccelRange = bmi270_ns.enum("BMI270AccelRange")
|
||||
ACCEL_RANGE_OPTIONS = {
|
||||
"2G": BMI270AccelRange.BMI270_ACCEL_RANGE_2G,
|
||||
@@ -77,13 +73,6 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_GYROSCOPE_ODR, default="200HZ"): cv.enum(
|
||||
GYRO_ODR_OPTIONS, upper=True
|
||||
),
|
||||
# Optional device wired to the BMI270's aux interface (not the main I2C bus).
|
||||
# Defaults to NONE so existing configs/boards are unaffected.
|
||||
cv.Optional(CONF_AUX_DEVICE, default="NONE"): cv.one_of(
|
||||
"NONE", "BMM150", upper=True
|
||||
),
|
||||
# Default matches the BMM150's fixed I2C address; irrelevant when aux_device is NONE.
|
||||
cv.Optional(CONF_AUX_ADDRESS, default=0x10): cv.i2c_address,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x68))
|
||||
@@ -101,9 +90,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_accel_odr(config[CONF_ACCELEROMETER_ODR]))
|
||||
cg.add(var.set_gyro_range(config[CONF_GYROSCOPE_RANGE]))
|
||||
cg.add(var.set_gyro_odr(config[CONF_GYROSCOPE_ODR]))
|
||||
# The aux interface code only compiles when some instance has a BMM150; the
|
||||
# instance still records its own device, as another one may have none
|
||||
if config[CONF_AUX_DEVICE] == "BMM150":
|
||||
cg.add_define("USE_BMI270_BMM150")
|
||||
cg.add(var.set_aux_device(BMI270AuxDevice.BMI270_AUX_DEVICE_BMM150))
|
||||
cg.add(var.set_aux_device_address(config[CONF_AUX_ADDRESS]))
|
||||
|
||||
@@ -6,97 +6,37 @@ from esphome.const import (
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_MAGNET,
|
||||
ICON_THERMOMETER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_MICROTESLA,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_BMI270_ID, BMI270Component, bmi270_ns
|
||||
from .motion import CONF_AUX_DEVICE
|
||||
from . import CONF_BMI270_ID, BMI270Component
|
||||
|
||||
AUTO_LOAD = ["bmi270"]
|
||||
|
||||
CONF_MAGNETIC_FIELD_X = "magnetic_field_x"
|
||||
CONF_MAGNETIC_FIELD_Y = "magnetic_field_y"
|
||||
CONF_MAGNETIC_FIELD_Z = "magnetic_field_z"
|
||||
|
||||
BMM150Data = bmi270_ns.namespace("bmm150").class_("BMM150Data")
|
||||
|
||||
_MAGNETIC_FIELDS = (CONF_MAGNETIC_FIELD_X, CONF_MAGNETIC_FIELD_Y, CONF_MAGNETIC_FIELD_Z)
|
||||
|
||||
|
||||
def _parent_schema() -> cv.Schema:
|
||||
return cv.Schema({cv.GenerateID(CONF_BMI270_ID): cv.use_id(BMI270Component)})
|
||||
|
||||
|
||||
def _temperature_sensor_schema() -> cv.Schema:
|
||||
return sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
).extend(_parent_schema())
|
||||
|
||||
|
||||
def _magnetic_field_sensor_schema() -> cv.Schema:
|
||||
return sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROTESLA,
|
||||
icon=ICON_MAGNET,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(_parent_schema())
|
||||
|
||||
|
||||
# `type` defaults to temperature so existing configs that omit it keep working unchanged.
|
||||
CONFIG_SCHEMA = cv.typed_schema(
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
).extend(
|
||||
{
|
||||
CONF_TEMPERATURE: _temperature_sensor_schema(),
|
||||
**{x: _magnetic_field_sensor_schema() for x in _MAGNETIC_FIELDS},
|
||||
},
|
||||
default_type=CONF_TEMPERATURE,
|
||||
cv.Optional(CONF_TYPE): cv.one_of(CONF_TEMPERATURE),
|
||||
cv.GenerateID(CONF_BMI270_ID): cv.use_id(BMI270Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if config[CONF_TYPE] not in _MAGNETIC_FIELDS:
|
||||
return
|
||||
full_config = fv.full_config.get()
|
||||
bmi270_path = full_config.get_path_for_id(config[CONF_BMI270_ID])[:-1]
|
||||
bmi270_config = full_config.get_config_for_path(bmi270_path)
|
||||
if bmi270_config.get(CONF_AUX_DEVICE) != "BMM150":
|
||||
raise cv.Invalid(
|
||||
"This sensor requires the parent bmi270's 'aux_device' to be set to 'BMM150'",
|
||||
path=[CONF_TYPE],
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
sensor_type = config[CONF_TYPE]
|
||||
var = await sensor.new_sensor(config)
|
||||
parent = await cg.get_variable(config[CONF_BMI270_ID])
|
||||
|
||||
if sensor_type == CONF_TEMPERATURE:
|
||||
data = MockObj("data")
|
||||
value_lambda = await cg.process_lambda(
|
||||
var.publish_state(data),
|
||||
[(cg.float_, str(data))],
|
||||
)
|
||||
cg.add(parent.add_temperature_listener(value_lambda))
|
||||
return
|
||||
|
||||
axis = sensor_type[-1:]
|
||||
data = MockObj("data")
|
||||
value_lambda = await cg.process_lambda(
|
||||
var.publish_state(getattr(data, axis)),
|
||||
[(BMM150Data.operator("const").operator("ref"), str(data))],
|
||||
var.publish_state(data),
|
||||
[(cg.float_, str(data))],
|
||||
)
|
||||
cg.add(parent.add_magnetometer_listener(value_lambda))
|
||||
cg.add(parent.add_temperature_listener(value_lambda))
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL, CONF_WATCHDOG
|
||||
from esphome.core import CORE, ID, EsphomeError, Lambda, TimePeriod
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
|
||||
CODEOWNERS = ["@Rapsssito"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "bq25186"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_BQ25186_ID = "bq25186_id"
|
||||
CONF_CONFIGURE_ON_BOOT = "configure_on_boot"
|
||||
CONF_BATTERY_REGULATION_VOLTAGE = "battery_regulation_voltage"
|
||||
CONF_FAST_CHARGE_CURRENT = "fast_charge_current"
|
||||
CONF_CHARGE_ENABLED = "charge_enabled"
|
||||
CONF_FLASH_CHARGING_MODE = "flash_charging_mode"
|
||||
CONF_PRECHARGE_IS_ITERM = "precharge_is_iterm"
|
||||
CONF_TERMINATION_PERCENT = "termination_percent"
|
||||
CONF_VINDPM_MODE = "vindpm_mode"
|
||||
CONF_THERMAL_REGULATION = "thermal_regulation"
|
||||
CONF_BATTERY_OCP_LIMIT = "battery_ocp_limit"
|
||||
CONF_BATTERY_UVLO = "battery_uvlo"
|
||||
CONF_MASK_CHARGE_STATUS_INTERRUPT = "mask_charge_status_interrupt"
|
||||
CONF_MASK_ILIM_INTERRUPT = "mask_ilim_interrupt"
|
||||
CONF_MASK_VINDPM_INTERRUPT = "mask_vindpm_interrupt"
|
||||
CONF_TS_AUTO_FUNCTION = "ts_auto_function"
|
||||
CONF_VLOWV = "vlowv"
|
||||
CONF_RECHARGE_THRESHOLD = "recharge_threshold"
|
||||
CONF_DOUBLE_TIMER_DURING_DPM = "double_timer_during_dpm"
|
||||
CONF_SAFETY_TIMER = "safety_timer"
|
||||
CONF_LONG_PRESS_DURATION = "long_press_duration"
|
||||
CONF_HW_RESET_REQUIRES_VIN = "hw_reset_requires_vin"
|
||||
CONF_AUTOWAKE = "autowake"
|
||||
CONF_INPUT_CURRENT_LIMIT = "input_current_limit"
|
||||
CONF_PUSH_BUTTON_LONG_PRESS_ACTION = "push_button_long_press_action"
|
||||
CONF_WAKE1_TIMER = "wake1_timer"
|
||||
CONF_WAKE2_TIMER = "wake2_timer"
|
||||
CONF_ENABLE_PUSH_BUTTON = "enable_push_button"
|
||||
CONF_SYSTEM_REGULATION = "system_regulation"
|
||||
CONF_SYS_MODE = "sys_mode"
|
||||
CONF_WATCHDOG_15S_ENABLE = "watchdog_15s_enable"
|
||||
CONF_DISABLE_VDPPM = "disable_vdppm"
|
||||
CONF_TS_HOT = "ts_hot"
|
||||
CONF_TS_COLD = "ts_cold"
|
||||
CONF_TS_WARM_DISABLE = "ts_warm_disable"
|
||||
CONF_TS_COOL_DISABLE = "ts_cool_disable"
|
||||
CONF_TS_ICHG = "ts_ichg"
|
||||
CONF_TS_VRCG = "ts_vrcg"
|
||||
CONF_MASK_TS_INTERRUPT = "mask_ts_interrupt"
|
||||
CONF_MASK_TREG_INTERRUPT = "mask_treg_interrupt"
|
||||
CONF_MASK_BAT_INTERRUPT = "mask_bat_interrupt"
|
||||
CONF_MASK_PG_INTERRUPT = "mask_pg_interrupt"
|
||||
|
||||
bq25186_ns = cg.esphome_ns.namespace("bq25186")
|
||||
BQ25186Component = bq25186_ns.class_(
|
||||
"BQ25186Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
BQ25186_CHILD_SCHEMA = cv.Schema(
|
||||
{cv.GenerateID(CONF_BQ25186_ID): cv.use_id(BQ25186Component)}
|
||||
)
|
||||
_request_binary_sensor_slot = cg.slot_counter("BQ25186_BINARY_SENSOR_COUNT")
|
||||
_request_text_sensor_slot = cg.slot_counter("BQ25186_TEXT_SENSOR_COUNT")
|
||||
|
||||
|
||||
def register_binary_sensor(hub: cg.MockObj, var: cg.MockObj) -> cg.Expression:
|
||||
_request_binary_sensor_slot(str(hub))
|
||||
return hub.register_binary_sensor(var)
|
||||
|
||||
|
||||
def register_text_sensor(hub: cg.MockObj, var: cg.MockObj) -> cg.Expression:
|
||||
_request_text_sensor_slot(str(hub))
|
||||
return hub.register_text_sensor(var)
|
||||
|
||||
|
||||
REG_VBAT_CTRL = 0x03
|
||||
REG_ICHG_CTRL = 0x04
|
||||
REG_CHARGECTRL0 = 0x05
|
||||
REG_CHARGECTRL1 = 0x06
|
||||
REG_IC_CTRL = 0x07
|
||||
REG_TMR_ILIM = 0x08
|
||||
REG_SHIP_RST = 0x09
|
||||
REG_SYS_REG = 0x0A
|
||||
REG_TS_CONTROL = 0x0B
|
||||
REG_MASK_ID = 0x0C
|
||||
REGISTER_COUNT = REG_MASK_ID - REG_VBAT_CTRL + 1
|
||||
|
||||
TERMINATION_PERCENT_OPTIONS = {"disabled": 0, "5%": 1, "10%": 2, "20%": 3}
|
||||
VINDPM_MODE_OPTIONS = {"battery_tracking": 0, "4.5v": 1, "4.7v": 2, "disabled": 3}
|
||||
THERM_REGULATION_OPTIONS = {"100c": 0, "80c": 1, "60c": 2, "disabled": 3}
|
||||
BATTERY_OCP_LIMIT_OPTIONS = {"500ma": 0, "1000ma": 1, "1500ma": 2, "3000ma": 3}
|
||||
BATTERY_UVLO_OPTIONS = {
|
||||
"3.0v": 0,
|
||||
"2.8v": 3,
|
||||
"2.6v": 4,
|
||||
"2.4v": 5,
|
||||
"2.2v": 6,
|
||||
"2.0v": 7,
|
||||
}
|
||||
VLOWV_OPTIONS = {"3.0v": 0, "2.8v": 1}
|
||||
RECHARGE_THRESHOLD_OPTIONS = {"100mv": 0, "200mv": 1}
|
||||
SAFETY_TIMER_OPTIONS = {"3h": 0, "6h": 1, "12h": 2, "disabled": 3}
|
||||
# The I2C watchdog restores the power-on registers when the bus stays quiet for its period
|
||||
WATCHDOG_OPTIONS = {
|
||||
"160s_software_reset": 0,
|
||||
"160s_hardware_reset": 1,
|
||||
"40s_hardware_reset": 2,
|
||||
"disabled": 3,
|
||||
}
|
||||
WATCHDOG_PERIOD_S = {
|
||||
"160s_software_reset": 160,
|
||||
"160s_hardware_reset": 160,
|
||||
"40s_hardware_reset": 40,
|
||||
}
|
||||
# SYS_REG watchdog_15s_enable: a hardware reset when the bus stays quiet for 15 s after VIN connects
|
||||
WATCHDOG_15S_PERIOD_S = 15
|
||||
# The chip boots with the 160 s software reset watchdog, which runs until the configuration is applied
|
||||
POWER_ON_WATCHDOG_PERIOD_S = 160
|
||||
LONG_PRESS_DURATION_OPTIONS = {"5s": 0, "10s": 1, "15s": 2, "20s": 3}
|
||||
AUTOWAKE_OPTIONS = {"0.5s": 0, "1s": 1, "2s": 2, "4s": 3}
|
||||
INPUT_CURRENT_LIMIT_OPTIONS = {
|
||||
"50ma": 0,
|
||||
"100ma": 1,
|
||||
"200ma": 2,
|
||||
"300ma": 3,
|
||||
"400ma": 4,
|
||||
"500ma": 5,
|
||||
"665ma": 6,
|
||||
"1050ma": 7,
|
||||
}
|
||||
PUSH_BUTTON_LONG_PRESS_ACTION_OPTIONS = {
|
||||
"none": 0,
|
||||
"hardware_reset": 1,
|
||||
"ship_mode": 2,
|
||||
"shutdown_mode": 3,
|
||||
}
|
||||
WAKE1_TIMER_OPTIONS = {"300ms": 0, "1s": 1}
|
||||
WAKE2_TIMER_OPTIONS = {"2s": 0, "3s": 1}
|
||||
SYSTEM_REGULATION_OPTIONS = {
|
||||
"battery_tracking": 0,
|
||||
"4.4v": 1,
|
||||
"4.5v": 2,
|
||||
"4.6v": 3,
|
||||
"4.7v": 4,
|
||||
"4.8v": 5,
|
||||
"4.9v": 6,
|
||||
"pass_through": 7,
|
||||
}
|
||||
SYS_MODE_OPTIONS = {
|
||||
"normal": 0,
|
||||
"battery_only": 1,
|
||||
"floating_off": 2,
|
||||
"pulldown_off": 3,
|
||||
}
|
||||
TS_HOT_OPTIONS = {"60c": 0, "65c": 1, "50c": 2, "45c": 3}
|
||||
TS_COLD_OPTIONS = {"0c": 0, "3c": 1, "5c": 2, "-3c": 3}
|
||||
TS_ICHG_OPTIONS = {"0.5x": 0, "0.2x": 1}
|
||||
TS_VRCG_OPTIONS = {"-100mv": 0, "-200mv": 1}
|
||||
|
||||
|
||||
def _on_voltage_step(volts: float) -> float:
|
||||
millivolts = volts * 1000
|
||||
if abs(millivolts - round(millivolts / 10) * 10) > 1e-6:
|
||||
raise cv.Invalid("battery_regulation_voltage must be a multiple of 10 mV")
|
||||
return volts
|
||||
|
||||
|
||||
def _on_current_step(amps: float) -> float:
|
||||
milliamps = amps * 1000
|
||||
step = 1 if milliamps < 36 else 10
|
||||
if abs(milliamps - round(milliamps / step) * step) > 1e-6 or 35 < milliamps < 40:
|
||||
raise cv.Invalid(
|
||||
"fast_charge_current must be a whole number of mA up to 35mA, then a multiple of 10 mA"
|
||||
)
|
||||
return amps
|
||||
|
||||
|
||||
# (config key, register, bit shift, validator or enum options, default).
|
||||
# VBAT_CTRL bit 7 (pg_mode) and SYS_REG bit 4 (pg_gpo_level) stay 0; the PG/GPO
|
||||
# switch drives them.
|
||||
FIELDS: tuple[tuple[str, int, int, Any, Any], ...] = (
|
||||
(
|
||||
CONF_BATTERY_REGULATION_VOLTAGE,
|
||||
REG_VBAT_CTRL,
|
||||
0,
|
||||
cv.All(cv.voltage, cv.Range(min=3.5, max=4.65), _on_voltage_step),
|
||||
"4.2V",
|
||||
),
|
||||
(CONF_CHARGE_ENABLED, REG_ICHG_CTRL, 7, cv.boolean, True),
|
||||
(
|
||||
CONF_FAST_CHARGE_CURRENT,
|
||||
REG_ICHG_CTRL,
|
||||
0,
|
||||
cv.All(cv.current, cv.Range(min=0.005, max=1.0), _on_current_step),
|
||||
"10mA",
|
||||
),
|
||||
(CONF_FLASH_CHARGING_MODE, REG_CHARGECTRL0, 7, cv.boolean, False),
|
||||
(CONF_PRECHARGE_IS_ITERM, REG_CHARGECTRL0, 6, cv.boolean, False),
|
||||
(CONF_TERMINATION_PERCENT, REG_CHARGECTRL0, 4, TERMINATION_PERCENT_OPTIONS, "10%"),
|
||||
(CONF_VINDPM_MODE, REG_CHARGECTRL0, 2, VINDPM_MODE_OPTIONS, "4.5v"),
|
||||
(CONF_THERMAL_REGULATION, REG_CHARGECTRL0, 0, THERM_REGULATION_OPTIONS, "100c"),
|
||||
(CONF_BATTERY_OCP_LIMIT, REG_CHARGECTRL1, 6, BATTERY_OCP_LIMIT_OPTIONS, "3000ma"),
|
||||
(CONF_BATTERY_UVLO, REG_CHARGECTRL1, 3, BATTERY_UVLO_OPTIONS, "3.0v"),
|
||||
(CONF_MASK_CHARGE_STATUS_INTERRUPT, REG_CHARGECTRL1, 2, cv.boolean, True),
|
||||
(CONF_MASK_ILIM_INTERRUPT, REG_CHARGECTRL1, 1, cv.boolean, True),
|
||||
(CONF_MASK_VINDPM_INTERRUPT, REG_CHARGECTRL1, 0, cv.boolean, False),
|
||||
(CONF_TS_AUTO_FUNCTION, REG_IC_CTRL, 7, cv.boolean, True),
|
||||
(CONF_VLOWV, REG_IC_CTRL, 6, VLOWV_OPTIONS, "3.0v"),
|
||||
(CONF_RECHARGE_THRESHOLD, REG_IC_CTRL, 5, RECHARGE_THRESHOLD_OPTIONS, "100mv"),
|
||||
(CONF_DOUBLE_TIMER_DURING_DPM, REG_IC_CTRL, 4, cv.boolean, False),
|
||||
(CONF_SAFETY_TIMER, REG_IC_CTRL, 2, SAFETY_TIMER_OPTIONS, "6h"),
|
||||
(CONF_WATCHDOG, REG_IC_CTRL, 0, WATCHDOG_OPTIONS, "disabled"),
|
||||
(CONF_LONG_PRESS_DURATION, REG_TMR_ILIM, 6, LONG_PRESS_DURATION_OPTIONS, "10s"),
|
||||
(CONF_HW_RESET_REQUIRES_VIN, REG_TMR_ILIM, 5, cv.boolean, False),
|
||||
(CONF_AUTOWAKE, REG_TMR_ILIM, 3, AUTOWAKE_OPTIONS, "1s"),
|
||||
(CONF_INPUT_CURRENT_LIMIT, REG_TMR_ILIM, 0, INPUT_CURRENT_LIMIT_OPTIONS, "500ma"),
|
||||
(
|
||||
CONF_PUSH_BUTTON_LONG_PRESS_ACTION,
|
||||
REG_SHIP_RST,
|
||||
3,
|
||||
PUSH_BUTTON_LONG_PRESS_ACTION_OPTIONS,
|
||||
"ship_mode",
|
||||
),
|
||||
(CONF_WAKE1_TIMER, REG_SHIP_RST, 2, WAKE1_TIMER_OPTIONS, "300ms"),
|
||||
(CONF_WAKE2_TIMER, REG_SHIP_RST, 1, WAKE2_TIMER_OPTIONS, "2s"),
|
||||
(CONF_ENABLE_PUSH_BUTTON, REG_SHIP_RST, 0, cv.boolean, True),
|
||||
(CONF_SYSTEM_REGULATION, REG_SYS_REG, 5, SYSTEM_REGULATION_OPTIONS, "4.5v"),
|
||||
(CONF_SYS_MODE, REG_SYS_REG, 2, SYS_MODE_OPTIONS, "normal"),
|
||||
(CONF_WATCHDOG_15S_ENABLE, REG_SYS_REG, 1, cv.boolean, False),
|
||||
(CONF_DISABLE_VDPPM, REG_SYS_REG, 0, cv.boolean, False),
|
||||
(CONF_TS_HOT, REG_TS_CONTROL, 6, TS_HOT_OPTIONS, "60c"),
|
||||
(CONF_TS_COLD, REG_TS_CONTROL, 4, TS_COLD_OPTIONS, "0c"),
|
||||
(CONF_TS_WARM_DISABLE, REG_TS_CONTROL, 3, cv.boolean, False),
|
||||
(CONF_TS_COOL_DISABLE, REG_TS_CONTROL, 2, cv.boolean, False),
|
||||
(CONF_TS_ICHG, REG_TS_CONTROL, 1, TS_ICHG_OPTIONS, "0.5x"),
|
||||
(CONF_TS_VRCG, REG_TS_CONTROL, 0, TS_VRCG_OPTIONS, "-100mv"),
|
||||
(CONF_MASK_TS_INTERRUPT, REG_MASK_ID, 7, cv.boolean, False),
|
||||
(CONF_MASK_TREG_INTERRUPT, REG_MASK_ID, 6, cv.boolean, True),
|
||||
(CONF_MASK_BAT_INTERRUPT, REG_MASK_ID, 5, cv.boolean, False),
|
||||
(CONF_MASK_PG_INTERRUPT, REG_MASK_ID, 4, cv.boolean, False),
|
||||
)
|
||||
|
||||
|
||||
def _battery_regulation_code(volts: float) -> int:
|
||||
"""VBAT_CTRL[6:0]: 3.5 V plus 10 mV per step."""
|
||||
return (round(volts * 1000) - 3500) // 10
|
||||
|
||||
|
||||
def _fast_charge_code(amps: float) -> int:
|
||||
"""ICHG_CTRL[6:0]: 1 mA steps from 5 mA to 35 mA, then 10 mA steps from 40 mA."""
|
||||
milliamps = round(amps * 1000)
|
||||
if milliamps <= 35:
|
||||
return milliamps - 5
|
||||
return milliamps // 10 + 27
|
||||
|
||||
|
||||
_ENCODERS: dict[str, Callable[[Any], int]] = {
|
||||
CONF_BATTERY_REGULATION_VOLTAGE: _battery_regulation_code,
|
||||
CONF_FAST_CHARGE_CURRENT: _fast_charge_code,
|
||||
CONF_CHARGE_ENABLED: lambda enabled: not enabled,
|
||||
}
|
||||
|
||||
|
||||
def pack_registers(config: ConfigType) -> list[int]:
|
||||
"""Pack the validated field values into the VBAT_CTRL..MASK_ID register bytes."""
|
||||
regs = [0] * REGISTER_COUNT
|
||||
for key, register, shift, validator, _ in FIELDS:
|
||||
value = config[key]
|
||||
if isinstance(validator, dict):
|
||||
value = validator[value]
|
||||
elif (encode := _ENCODERS.get(key)) is not None:
|
||||
value = encode(value)
|
||||
regs[register - REG_VBAT_CTRL] |= int(value) << shift
|
||||
return regs
|
||||
|
||||
|
||||
def _field_schema(with_defaults: bool) -> cv.Schema:
|
||||
schema = {}
|
||||
for key, _, _, validator, default in FIELDS:
|
||||
if isinstance(validator, dict):
|
||||
validator = cv.enum(validator, lower=True)
|
||||
optional = (
|
||||
cv.Optional(key, default=default) if with_defaults else cv.Optional(key)
|
||||
)
|
||||
schema[optional] = validator
|
||||
return cv.Schema(schema)
|
||||
|
||||
|
||||
BQ25186_CONFIG_SCHEMA = _field_schema(with_defaults=True)
|
||||
|
||||
|
||||
def _check_watchdog(config: ConfigType) -> ConfigType:
|
||||
"""Only the status poll talks to the chip, so it has to come round before any watchdog does."""
|
||||
periods = []
|
||||
if (period := WATCHDOG_PERIOD_S.get(config[CONF_WATCHDOG])) is not None:
|
||||
periods.append(period)
|
||||
if config[CONF_WATCHDOG_15S_ENABLE]:
|
||||
periods.append(WATCHDOG_15S_PERIOD_S)
|
||||
if not config.get(CONF_CONFIGURE_ON_BOOT, True):
|
||||
periods.append(POWER_ON_WATCHDOG_PERIOD_S)
|
||||
if not periods:
|
||||
return config
|
||||
if config[CONF_UPDATE_INTERVAL] >= TimePeriod(seconds=(limit := min(periods))):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_UPDATE_INTERVAL} must be shorter than {limit} s, or the "
|
||||
f"watchdog resets the charger between status polls",
|
||||
path=[CONF_UPDATE_INTERVAL],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BQ25186Component),
|
||||
cv.Optional(CONF_CONFIGURE_ON_BOOT, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(BQ25186_CONFIG_SCHEMA)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x6A)),
|
||||
_check_watchdog,
|
||||
)
|
||||
|
||||
CONFIGURE_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(BQ25186Component),
|
||||
}
|
||||
).extend(_field_schema(with_defaults=False))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if config[CONF_CONFIGURE_ON_BOOT]:
|
||||
cg.add(var.set_setup_registers(cg.ArrayInitializer(*pack_registers(config))))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bq25186.apply_configuration",
|
||||
automation.LambdaAction,
|
||||
CONFIGURE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def bq25186_configure_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> cg.MockObj:
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
# Options the action leaves out fall back to the component's configuration
|
||||
path = CORE.config.get_path_for_id(config[CONF_ID])[:-1]
|
||||
parent_config = CORE.config.get_config_for_path(path)
|
||||
merged = {**parent_config, **config}
|
||||
try:
|
||||
_check_watchdog(merged)
|
||||
except cv.Invalid as err:
|
||||
raise EsphomeError(f"bq25186.apply_configuration: {err.msg}") from err
|
||||
regs = pack_registers(merged)
|
||||
|
||||
text = str(cg.statement(parent.apply_config(cg.ArrayInitializer(*regs))))
|
||||
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
|
||||
return automation.new_lambda_pvariable(
|
||||
action_id, lambda_, automation.StatelessLambdaAction, template_arg
|
||||
)
|
||||
@@ -1,73 +0,0 @@
|
||||
from functools import partial
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_BATTERY_CHARGING,
|
||||
DEVICE_CLASS_OPENING,
|
||||
DEVICE_CLASS_PLUG,
|
||||
DEVICE_CLASS_RUNNING,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns, register_binary_sensor
|
||||
|
||||
DEPENDENCIES = ["bq25186"]
|
||||
|
||||
BQ25186BinarySensor = bq25186_ns.class_(
|
||||
"BQ25186BinarySensor", binary_sensor.BinarySensor
|
||||
)
|
||||
|
||||
REG_STAT0 = 0
|
||||
REG_STAT1 = 1
|
||||
REG_FLAG0 = 2
|
||||
# Bit set for each field value that reads as on: a single bit is on when it is 1
|
||||
_BIT_SET = 0b10
|
||||
|
||||
# (key, status register, bit shift, field mask, on-values bitmap, device class)
|
||||
_FIELDS = (
|
||||
("ts", REG_STAT0, 7, 0x01, _BIT_SET, DEVICE_CLASS_OPENING),
|
||||
("charging", REG_STAT0, 5, 0x03, 0b0110, DEVICE_CLASS_BATTERY_CHARGING),
|
||||
("charge_done", REG_STAT0, 5, 0x03, 0b1000, cv.UNDEFINED),
|
||||
("ilim", REG_STAT0, 4, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("vdppm", REG_STAT0, 3, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("vindpm", REG_STAT0, 2, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("thermreg", REG_STAT0, 1, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("power_good", REG_STAT0, 0, 0x01, _BIT_SET, DEVICE_CLASS_PLUG),
|
||||
("vin_ovp", REG_STAT1, 7, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("battery_uvlo", REG_STAT1, 6, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("safety_timer_flag", REG_STAT1, 2, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("wake1_flag", REG_STAT1, 1, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("wake2_flag", REG_STAT1, 0, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("ts_flag", REG_FLAG0, 7, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("ilim_flag", REG_FLAG0, 6, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("vdppm_flag", REG_FLAG0, 5, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("vindpm_flag", REG_FLAG0, 4, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("thermreg_flag", REG_FLAG0, 3, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("vin_ovp_flag", REG_FLAG0, 2, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("battery_uvlo_flag", REG_FLAG0, 1, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("battery_ocp_flag", REG_FLAG0, 0, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
BQ25186_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(key): binary_sensor.binary_sensor_schema(
|
||||
BQ25186BinarySensor, device_class=device_class
|
||||
)
|
||||
for key, *_, device_class in _FIELDS
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(*(key for key, *_ in _FIELDS)),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_BQ25186_ID])
|
||||
sensors = binary_sensor.sub_binary_sensors(config)
|
||||
for key, reg, shift, mask, match, _ in _FIELDS:
|
||||
await sensors(
|
||||
key, partial(register_binary_sensor, hub), reg, shift, mask, match
|
||||
)
|
||||
@@ -1,175 +0,0 @@
|
||||
#include "bq25186.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "bq25186_binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "bq25186_text_sensor.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bq25186");
|
||||
|
||||
// Bits the configuration owns in each register; the rest are the SYS_REG GPO level, reset
|
||||
// triggers (SHIP_RST) or the device id (MASK_ID) and are kept as read
|
||||
static const uint8_t BQ25186_CONFIG_MASKS[BQ25186_CONFIG_REGISTER_COUNT] PROGMEM = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xEF, 0xFF, 0xF0,
|
||||
};
|
||||
|
||||
bool BQ25186Component::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 BQ25186Component::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", reg);
|
||||
return false;
|
||||
}
|
||||
reg_value = (reg_value & ~mask) | (value & mask);
|
||||
return this->write_register_(reg, reg_value);
|
||||
}
|
||||
|
||||
uint8_t BQ25186Component::expected_register_(size_t index) const {
|
||||
uint8_t value = this->regs_[index];
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
if (index == 0 && this->pg_gpo_switch_ != nullptr)
|
||||
value |= BQ25186_VBAT_CTRL_PG_MODE;
|
||||
#endif
|
||||
return value;
|
||||
}
|
||||
|
||||
bool BQ25186Component::config_matches_(const BQ25186StatusRegisters ®s) const {
|
||||
for (uint8_t reg = BQ25186_REG_VBAT_CTRL; reg <= BQ25186_REG_MASK_ID; reg++) {
|
||||
const size_t index = reg - BQ25186_REG_VBAT_CTRL;
|
||||
const uint8_t mask = progmem_read_byte(&BQ25186_CONFIG_MASKS[index]);
|
||||
if (((regs[reg] ^ this->expected_register_(index)) & mask) != 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BQ25186Component::apply_config(const BQ25186ConfigRegisters ®s) {
|
||||
// Setup rejects an unknown chip; the action must not program it before that check or after it failed
|
||||
if (!this->is_ready() || this->is_failed()) {
|
||||
ESP_LOGW(TAG, "Not applying configuration, setup has not succeeded");
|
||||
return false;
|
||||
}
|
||||
this->regs_ = regs;
|
||||
this->configured_ = true;
|
||||
for (size_t i = 0; i < regs.size(); i++) {
|
||||
const uint8_t reg = BQ25186_REG_VBAT_CTRL + i;
|
||||
const uint8_t mask = progmem_read_byte(&BQ25186_CONFIG_MASKS[i]);
|
||||
const uint8_t value = this->expected_register_(i);
|
||||
const bool ok = mask == 0xFF ? this->write_register_(reg, value) : this->update_register_(reg, mask, value);
|
||||
if (!ok) {
|
||||
this->status_set_warning(LOG_STR("Configuration partly applied"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->status_clear_warning();
|
||||
return true;
|
||||
}
|
||||
|
||||
void BQ25186Component::setup() {
|
||||
uint8_t mask_id;
|
||||
if (!this->read_byte(BQ25186_REG_MASK_ID, &mask_id)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Another charger at this address could lay its registers out differently, so do not write into it
|
||||
const uint8_t device_id = mask_id & 0x0F;
|
||||
if (device_id != 0x01) {
|
||||
ESP_LOGE(TAG, "Unexpected device id 0x%X (expected 0x1)", device_id);
|
||||
this->mark_failed(LOG_STR("Not a BQ25186"));
|
||||
return;
|
||||
}
|
||||
|
||||
// A failed write only sets the warning status: the chip has identified itself, so a later
|
||||
// apply_configuration may retry
|
||||
if (this->configure_on_boot_)
|
||||
this->apply_config(this->regs_);
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
if (this->pg_gpo_switch_ != nullptr) {
|
||||
// The poll below puts the pin in output mode; without a restore mode the switch takes its level from it
|
||||
auto initial = this->pg_gpo_switch_->get_initial_state_with_restore_mode();
|
||||
if (initial.has_value())
|
||||
this->pg_gpo_switch_->publish_state(*initial); // the first poll writes it to the pin
|
||||
}
|
||||
#endif
|
||||
// The poller first runs after a random share of the interval; the entities get a state now
|
||||
this->update();
|
||||
}
|
||||
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
// The pin mode and level come from the switch, so every poll puts back whatever the chip lost
|
||||
void BQ25186Component::check_pg_gpo_(const BQ25186StatusRegisters &status, bool &pending) {
|
||||
if (this->pg_gpo_switch_ == nullptr)
|
||||
return;
|
||||
if ((status[BQ25186_REG_VBAT_CTRL] & BQ25186_VBAT_CTRL_PG_MODE) == 0 &&
|
||||
!this->update_register_(BQ25186_REG_VBAT_CTRL, BQ25186_VBAT_CTRL_PG_MODE, BQ25186_VBAT_CTRL_PG_MODE)) {
|
||||
this->status_set_warning(LOG_STR("PG/GPO pin not in output mode"));
|
||||
pending = true;
|
||||
}
|
||||
const bool level = (status[BQ25186_REG_SYS_REG] & BQ25186_SYS_REG_PG_GPO_LEVEL) != 0;
|
||||
if (!this->pg_gpo_switch_->has_state()) {
|
||||
this->pg_gpo_switch_->publish_state(level);
|
||||
} else if (level != this->pg_gpo_switch_->state && !this->write_pg_gpo_level(this->pg_gpo_switch_->state)) {
|
||||
this->status_set_warning(LOG_STR("PG/GPO level write failed"));
|
||||
pending = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Every poll also serves the I2C watchdog, which wants to see traffic within its period
|
||||
void BQ25186Component::update() {
|
||||
BQ25186StatusRegisters status;
|
||||
if (!this->read_bytes(BQ25186_REG_STAT0, status.data(), status.size())) {
|
||||
this->status_set_warning(LOG_STR("Status read failed"));
|
||||
return;
|
||||
}
|
||||
// Whatever failed is tried again here, so the warning only stays while something is still wrong
|
||||
bool pending = false;
|
||||
// A watchdog or hardware reset puts the power-on values back, and a failed write leaves a register
|
||||
// behind; either way the configuration is written again
|
||||
if (this->configured_ && !this->config_matches_(status)) {
|
||||
ESP_LOGW(TAG, "Configuration registers differ from the values applied, writing them again");
|
||||
if (this->apply_config(this->regs_)) {
|
||||
status[BQ25186_REG_VBAT_CTRL] |= BQ25186_VBAT_CTRL_PG_MODE; // part of the write when a switch is configured
|
||||
} else {
|
||||
pending = true;
|
||||
}
|
||||
}
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
this->check_pg_gpo_(status, pending);
|
||||
#endif
|
||||
if (!pending)
|
||||
this->status_clear_warning();
|
||||
#ifdef BQ25186_BINARY_SENSOR_COUNT
|
||||
for (auto *sensor : this->binary_sensors_)
|
||||
sensor->on_status(status);
|
||||
#endif
|
||||
#ifdef BQ25186_TEXT_SENSOR_COUNT
|
||||
for (auto *sensor : this->text_sensors_)
|
||||
sensor->on_status(status);
|
||||
#endif
|
||||
}
|
||||
|
||||
void BQ25186Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BQ25186:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
LOG_SWITCH(" ", "PG/GPO", this->pg_gpo_switch_);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
@@ -1,95 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
static constexpr uint8_t BQ25186_REG_STAT0 = 0x00;
|
||||
static constexpr uint8_t BQ25186_REG_STAT1 = 0x01;
|
||||
static constexpr uint8_t BQ25186_REG_FLAG0 = 0x02;
|
||||
static constexpr uint8_t BQ25186_REG_VBAT_CTRL = 0x03;
|
||||
static constexpr uint8_t BQ25186_REG_ICHG_CTRL = 0x04;
|
||||
static constexpr uint8_t BQ25186_REG_CHARGECTRL0 = 0x05;
|
||||
static constexpr uint8_t BQ25186_REG_CHARGECTRL1 = 0x06;
|
||||
static constexpr uint8_t BQ25186_REG_IC_CTRL = 0x07;
|
||||
static constexpr uint8_t BQ25186_REG_TMR_ILIM = 0x08;
|
||||
static constexpr uint8_t BQ25186_REG_SHIP_RST = 0x09;
|
||||
static constexpr uint8_t BQ25186_REG_SYS_REG = 0x0A;
|
||||
static constexpr uint8_t BQ25186_REG_TS_CONTROL = 0x0B;
|
||||
static constexpr uint8_t BQ25186_REG_MASK_ID = 0x0C;
|
||||
static constexpr size_t BQ25186_CONFIG_REGISTER_COUNT = BQ25186_REG_MASK_ID - BQ25186_REG_VBAT_CTRL + 1;
|
||||
// STAT0 to MASK_ID, read together by every poll: the status for the entities, then the configuration
|
||||
// registers, which show whether the chip was reset or a write failed
|
||||
static constexpr size_t BQ25186_STATUS_REGISTER_COUNT = BQ25186_REG_MASK_ID - BQ25186_REG_STAT0 + 1;
|
||||
|
||||
// VBAT_CTRL pg_mode: the /PG pin is a general purpose output driven by SYS_REG pg_gpo_level
|
||||
static constexpr uint8_t BQ25186_VBAT_CTRL_PG_MODE = 0x80;
|
||||
static constexpr uint8_t BQ25186_SYS_REG_PG_GPO_LEVEL = 0x10;
|
||||
|
||||
// Register values for VBAT_CTRL..MASK_ID, packed by code generation
|
||||
using BQ25186ConfigRegisters = std::array<uint8_t, BQ25186_CONFIG_REGISTER_COUNT>;
|
||||
using BQ25186StatusRegisters = std::array<uint8_t, BQ25186_STATUS_REGISTER_COUNT>;
|
||||
|
||||
class BQ25186BinarySensor;
|
||||
class BQ25186TextSensor;
|
||||
|
||||
class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_setup_registers(const BQ25186ConfigRegisters ®s) {
|
||||
this->regs_ = regs;
|
||||
this->configure_on_boot_ = true;
|
||||
}
|
||||
bool apply_config(const BQ25186ConfigRegisters ®s);
|
||||
#ifdef BQ25186_BINARY_SENSOR_COUNT
|
||||
void register_binary_sensor(BQ25186BinarySensor *sensor) { this->binary_sensors_.push_back(sensor); }
|
||||
#endif
|
||||
#ifdef BQ25186_TEXT_SENSOR_COUNT
|
||||
void register_text_sensor(BQ25186TextSensor *sensor) { this->text_sensors_.push_back(sensor); }
|
||||
#endif
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
/// The /PG/GPO pin is put in general purpose output mode when a switch drives it
|
||||
void set_pg_gpo_switch(switch_::Switch *pg_gpo_switch) { this->pg_gpo_switch_ = pg_gpo_switch; }
|
||||
bool write_pg_gpo_level(bool level) {
|
||||
return this->update_register_(BQ25186_REG_SYS_REG, BQ25186_SYS_REG_PG_GPO_LEVEL,
|
||||
level ? BQ25186_SYS_REG_PG_GPO_LEVEL : 0);
|
||||
}
|
||||
#endif
|
||||
/// Write the masked bits of SHIP_RST
|
||||
bool write_ship_rst(uint8_t mask, uint8_t value) { return this->update_register_(BQ25186_REG_SHIP_RST, mask, value); }
|
||||
|
||||
protected:
|
||||
bool write_register_(uint8_t reg, uint8_t value);
|
||||
uint8_t expected_register_(size_t index) const;
|
||||
bool config_matches_(const BQ25186StatusRegisters ®s) const;
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
void check_pg_gpo_(const BQ25186StatusRegisters &status, bool &pending);
|
||||
#endif
|
||||
bool update_register_(uint8_t reg, uint8_t mask, uint8_t value);
|
||||
|
||||
#ifdef BQ25186_BINARY_SENSOR_COUNT
|
||||
StaticVector<BQ25186BinarySensor *, BQ25186_BINARY_SENSOR_COUNT> binary_sensors_;
|
||||
#endif
|
||||
#ifdef BQ25186_TEXT_SENSOR_COUNT
|
||||
StaticVector<BQ25186TextSensor *, BQ25186_TEXT_SENSOR_COUNT> text_sensors_;
|
||||
#endif
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
switch_::Switch *pg_gpo_switch_{nullptr};
|
||||
#endif
|
||||
BQ25186ConfigRegisters regs_{}; // the configuration last applied, written again after a chip reset
|
||||
bool configure_on_boot_{false};
|
||||
bool configured_{false}; // regs_ went to the chip at least once
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
#include "bq25186.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
/// One field of a status register; `match` has a bit set for each field value that reads as on
|
||||
class BQ25186BinarySensor final : public binary_sensor::BinarySensor {
|
||||
public:
|
||||
BQ25186BinarySensor(uint8_t reg, uint8_t shift, uint8_t mask, uint8_t match)
|
||||
: reg_(reg), shift_(shift), mask_(mask), match_(match) {}
|
||||
|
||||
void on_status(const BQ25186StatusRegisters &status) {
|
||||
const uint8_t value = (status[this->reg_] >> this->shift_) & this->mask_;
|
||||
this->publish_state((this->match_ >> value) & 1);
|
||||
}
|
||||
|
||||
protected:
|
||||
const uint8_t reg_;
|
||||
const uint8_t shift_;
|
||||
const uint8_t mask_;
|
||||
const uint8_t match_;
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_BINARY_SENSOR
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BUTTON
|
||||
|
||||
#include "bq25186.h"
|
||||
#include "esphome/components/button/button.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
/// Writes one SHIP_RST field when pressed
|
||||
class BQ25186Button final : public button::Button, public Parented<BQ25186Component> {
|
||||
public:
|
||||
BQ25186Button(uint8_t mask, uint8_t value) : mask_(mask), value_(value) {}
|
||||
|
||||
protected:
|
||||
void press_action() override { this->parent_->write_ship_rst(this->mask_, this->value_); }
|
||||
|
||||
const uint8_t mask_;
|
||||
const uint8_t value_;
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_BUTTON
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
|
||||
#include "bq25186.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
class BQ25186PgGpoSwitch final : public switch_::Switch, public Parented<BQ25186Component> {
|
||||
protected:
|
||||
void write_state(bool state) override {
|
||||
if (this->parent_->write_pg_gpo_level(state))
|
||||
this->publish_state(state);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_BQ25186_PG_GPO
|
||||
@@ -1,26 +0,0 @@
|
||||
#include "bq25186_text_sensor.h"
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
// STAT0[6:5] charge status, then STAT1[4:3] TS status
|
||||
PROGMEM_STRING_TABLE(StatusStrings, "Not Charging", "Constant Current", "Constant Voltage", "Charge Done/Disabled",
|
||||
"Normal", "Hot/Cold (Charging Suspended)", "Cool (Current Reduced)", "Warm (Voltage Reduced)");
|
||||
|
||||
void BQ25186TextSensor::on_status(const BQ25186StatusRegisters &status) {
|
||||
const uint8_t value = (status[this->reg_] >> this->shift_) & 0x03;
|
||||
if (value == this->last_)
|
||||
return;
|
||||
this->last_ = value;
|
||||
const ProgmemStr name = StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX);
|
||||
char text[32];
|
||||
ESPHOME_strncpy_P(text, reinterpret_cast<ESPHOME_PGM_P>(name), sizeof(text));
|
||||
text[sizeof(text) - 1] = '\0';
|
||||
this->publish_state(text);
|
||||
}
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_TEXT_SENSOR
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
||||
#include "bq25186.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
/// A two-bit status field; `first` is the string table index of the field's value 0
|
||||
class BQ25186TextSensor final : public text_sensor::TextSensor {
|
||||
public:
|
||||
BQ25186TextSensor(uint8_t reg, uint8_t shift, uint8_t first) : reg_(reg), shift_(shift), first_(first) {}
|
||||
void on_status(const BQ25186StatusRegisters &status);
|
||||
|
||||
protected:
|
||||
const uint8_t reg_;
|
||||
const uint8_t shift_;
|
||||
const uint8_t first_;
|
||||
uint8_t last_{0xFF}; // the field value last published
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_TEXT_SENSOR
|
||||
@@ -1,52 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_RESTART,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_POWER,
|
||||
ICON_RESTART,
|
||||
ICON_RESTART_ALERT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns
|
||||
|
||||
DEPENDENCIES = ["bq25186"]
|
||||
|
||||
BQ25186Button = bq25186_ns.class_("BQ25186Button", button.Button)
|
||||
|
||||
# SHIP_RST: bit 7 is the software reset, bits 6:5 the shutdown, ship and hardware reset codes
|
||||
SOFTWARE_RESET_MASK = 0x80
|
||||
MODE_MASK = 0x60
|
||||
|
||||
# (key, SHIP_RST mask, value, device class, icon)
|
||||
_BUTTONS = (
|
||||
("software_reset", SOFTWARE_RESET_MASK, 0x80, DEVICE_CLASS_RESTART, ICON_RESTART),
|
||||
("shutdown", MODE_MASK, 0x20, cv.UNDEFINED, ICON_POWER),
|
||||
("ship_mode", MODE_MASK, 0x40, cv.UNDEFINED, ICON_POWER),
|
||||
("hardware_reset", MODE_MASK, 0x60, DEVICE_CLASS_RESTART, ICON_RESTART_ALERT),
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
BQ25186_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(key): button.button_schema(
|
||||
BQ25186Button,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=icon,
|
||||
device_class=device_class,
|
||||
)
|
||||
for key, _, _, device_class, icon in _BUTTONS
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(*(key for key, *_ in _BUTTONS)),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
for key, mask, value, _, _ in _BUTTONS:
|
||||
if (conf := config.get(key)) is not None:
|
||||
var = await button.new_button(conf, mask, value)
|
||||
await cg.register_parented(var, config[CONF_BQ25186_ID])
|
||||
@@ -1,27 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns
|
||||
|
||||
DEPENDENCIES = ["bq25186"]
|
||||
|
||||
CONF_PG_GPO = "pg_gpo"
|
||||
|
||||
BQ25186PgGpoSwitch = bq25186_ns.class_("BQ25186PgGpoSwitch", switch.Switch)
|
||||
|
||||
CONFIG_SCHEMA = BQ25186_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_PG_GPO): switch.switch_schema(
|
||||
BQ25186PgGpoSwitch, default_restore_mode="DISABLED"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_BQ25186_PG_GPO")
|
||||
hub = await cg.get_variable(config[CONF_BQ25186_ID])
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(CONF_PG_GPO, hub.set_pg_gpo_switch)
|
||||
@@ -1,38 +0,0 @@
|
||||
from functools import partial
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns, register_text_sensor
|
||||
|
||||
DEPENDENCIES = ["bq25186"]
|
||||
|
||||
BQ25186TextSensor = bq25186_ns.class_("BQ25186TextSensor", text_sensor.TextSensor)
|
||||
|
||||
REG_STAT0 = 0
|
||||
REG_STAT1 = 1
|
||||
|
||||
# (key, status register, bit shift, index of the field's value 0 in the C++ string table)
|
||||
_FIELDS = (
|
||||
("charge_status", REG_STAT0, 5, 0),
|
||||
("ts_status", REG_STAT1, 3, 4),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
BQ25186_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(key): text_sensor.text_sensor_schema(BQ25186TextSensor)
|
||||
for key, *_ in _FIELDS
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(*(key for key, *_ in _FIELDS)),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_BQ25186_ID])
|
||||
sensors = text_sensor.sub_text_sensors(config)
|
||||
for key, reg, shift, first in _FIELDS:
|
||||
await sensors(key, partial(register_text_sensor, hub), reg, shift, first)
|
||||
@@ -1,2 +0,0 @@
|
||||
CODEOWNERS = ["@rggammon"]
|
||||
DOMAIN = "bq27220"
|
||||
@@ -1,93 +0,0 @@
|
||||
#include "bq27220.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::bq27220 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bq27220");
|
||||
|
||||
// Standard command registers (16-bit, little-endian), SLUUBD4A
|
||||
static constexpr uint8_t REG_CONTROL = 0x00;
|
||||
static constexpr uint8_t REG_TEMPERATURE = 0x06; // 0.1 K
|
||||
static constexpr uint8_t REG_VOLTAGE = 0x08; // mV
|
||||
static constexpr uint8_t REG_CURRENT = 0x0C; // mA, signed
|
||||
static constexpr uint8_t REG_REMAINING_CAPACITY = 0x10; // mAh
|
||||
static constexpr uint8_t REG_FULL_CHARGE_CAPACITY = 0x12; // mAh
|
||||
static constexpr uint8_t REG_TIME_TO_EMPTY = 0x16; // min, 0xFFFF when not discharging
|
||||
static constexpr uint8_t REG_STATE_OF_CHARGE = 0x2C; // %
|
||||
static constexpr uint8_t REG_STATE_OF_HEALTH = 0x2E; // %
|
||||
// The DEVICE_NUMBER subcommand (0x0001, SLUUBD4A §2.2.2) answers through the MAC data buffer
|
||||
static constexpr uint8_t REG_MAC_DATA = 0x40;
|
||||
static constexpr uint16_t DEVICE_NUMBER = 0x0220;
|
||||
static constexpr uint16_t TIME_TO_EMPTY_NOT_APPLICABLE = 0xFFFF;
|
||||
|
||||
bool BQ27220Component::read_word_(uint8_t reg, uint16_t &value) {
|
||||
uint8_t data[2];
|
||||
if (this->read_register(reg, data, 2) != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
value = encode_uint16(data[1], data[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BQ27220Component::setup() {
|
||||
const uint8_t device_number_cmd[2] = {0x01, 0x00}; // DEVICE_NUMBER subcommand, little-endian
|
||||
if (this->write_register(REG_CONTROL, device_number_cmd, 2) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR("Failed to communicate with BQ27220"));
|
||||
return;
|
||||
}
|
||||
delay(2); // SLUUBD4A §5.3: the subcommand result needs a moment before it can be read
|
||||
uint16_t device_number = 0;
|
||||
if (!this->read_word_(REG_MAC_DATA, device_number)) {
|
||||
this->mark_failed(LOG_STR("Failed to read device number from BQ27220"));
|
||||
return;
|
||||
}
|
||||
if (device_number != DEVICE_NUMBER) {
|
||||
ESP_LOGE(TAG, "Unexpected device number 0x%04X (expected 0x%04X)", device_number, DEVICE_NUMBER);
|
||||
this->mark_failed();
|
||||
}
|
||||
}
|
||||
|
||||
void BQ27220Component::update() {
|
||||
bool ok = true;
|
||||
this->publish_(this->voltage_sensor_, REG_VOLTAGE, ok, [](uint16_t raw) { return raw * 0.001f; });
|
||||
this->publish_(this->current_sensor_, REG_CURRENT, ok,
|
||||
[](uint16_t raw) { return static_cast<int16_t>(raw) * 0.001f; });
|
||||
this->publish_(this->battery_level_sensor_, REG_STATE_OF_CHARGE, ok, [](uint16_t raw) { return float(raw); });
|
||||
this->publish_(this->temperature_sensor_, REG_TEMPERATURE, ok, [](uint16_t raw) { return raw * 0.1f - 273.15f; });
|
||||
this->publish_(this->remaining_capacity_sensor_, REG_REMAINING_CAPACITY, ok, [](uint16_t raw) { return float(raw); });
|
||||
this->publish_(this->full_charge_capacity_sensor_, REG_FULL_CHARGE_CAPACITY, ok,
|
||||
[](uint16_t raw) { return float(raw); });
|
||||
this->publish_(this->time_to_empty_sensor_, REG_TIME_TO_EMPTY, ok,
|
||||
[](uint16_t raw) { return raw == TIME_TO_EMPTY_NOT_APPLICABLE ? NAN : float(raw); });
|
||||
this->publish_(this->state_of_health_sensor_, REG_STATE_OF_HEALTH, ok, [](uint16_t raw) { return float(raw); });
|
||||
|
||||
if (ok) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->status_set_warning(LOG_STR("Failed to read one or more registers from BQ27220"));
|
||||
}
|
||||
}
|
||||
|
||||
void BQ27220Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BQ27220 Battery Fuel Gauge:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current", this->current_sensor_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Remaining Capacity", this->remaining_capacity_sensor_);
|
||||
LOG_SENSOR(" ", "Full Charge Capacity", this->full_charge_capacity_sensor_);
|
||||
LOG_SENSOR(" ", "Time To Empty", this->time_to_empty_sensor_);
|
||||
LOG_SENSOR(" ", "State Of Health", this->state_of_health_sensor_);
|
||||
}
|
||||
|
||||
} // namespace esphome::bq27220
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// Datasheet: https://www.ti.com/lit/ds/symlink/bq27220.pdf
|
||||
// Technical Reference Manual: https://www.ti.com/lit/ug/sluubd4a/sluubd4a.pdf
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::bq27220 {
|
||||
|
||||
class BQ27220Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
SUB_SENSOR(voltage)
|
||||
SUB_SENSOR(current)
|
||||
SUB_SENSOR(battery_level)
|
||||
SUB_SENSOR(temperature)
|
||||
SUB_SENSOR(remaining_capacity)
|
||||
SUB_SENSOR(full_charge_capacity)
|
||||
SUB_SENSOR(time_to_empty)
|
||||
SUB_SENSOR(state_of_health)
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
bool read_word_(uint8_t reg, uint16_t &value);
|
||||
/// Read a standard command register and publish it through the sensor, if one is configured.
|
||||
/// A failed read publishes NAN and clears `ok`.
|
||||
template<typename F> void publish_(sensor::Sensor *sensor, uint8_t reg, bool &ok, F convert) {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
uint16_t raw;
|
||||
if (!this->read_word_(reg, raw)) {
|
||||
ok = false;
|
||||
sensor->publish_state(NAN);
|
||||
return;
|
||||
}
|
||||
sensor->publish_state(convert(raw));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::bq27220
|
||||
@@ -1,123 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_MILLIAMPERE_HOUR
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_CURRENT,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_VOLTAGE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_BATTERY,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_MINUTE,
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
bq27220_ns = cg.esphome_ns.namespace("bq27220")
|
||||
BQ27220Component = bq27220_ns.class_(
|
||||
"BQ27220Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONF_REMAINING_CAPACITY = "remaining_capacity"
|
||||
CONF_FULL_CHARGE_CAPACITY = "full_charge_capacity"
|
||||
CONF_TIME_TO_EMPTY = "time_to_empty"
|
||||
CONF_STATE_OF_HEALTH = "state_of_health"
|
||||
|
||||
# Other SLUUBD4A standard commands are intentionally not exposed to keep the
|
||||
# component focused; they can be added later if there is demand: TimeToFull
|
||||
# (0x18), CycleCount (0x2A), BatteryStatus (0x0A), AveragePower (0x24),
|
||||
# DesignCapacity (0x3C).
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BQ27220Component),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
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,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_REMAINING_CAPACITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MILLIAMPERE_HOUR,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon=ICON_BATTERY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_FULL_CHARGE_CAPACITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MILLIAMPERE_HOUR,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon=ICON_BATTERY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_TIME_TO_EMPTY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MINUTE,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_STATE_OF_HEALTH): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon=ICON_BATTERY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x55))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
await sensors(CONF_CURRENT, var.set_current_sensor)
|
||||
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level_sensor)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
await sensors(CONF_REMAINING_CAPACITY, var.set_remaining_capacity_sensor)
|
||||
await sensors(CONF_FULL_CHARGE_CAPACITY, var.set_full_charge_capacity_sensor)
|
||||
await sensors(CONF_TIME_TO_EMPTY, var.set_time_to_empty_sensor)
|
||||
await sensors(CONF_STATE_OF_HEALTH, var.set_state_of_health_sensor)
|
||||
@@ -1,111 +0,0 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["sensor"]
|
||||
CODEOWNERS = ["@limpkin"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "bq76972"
|
||||
MULTI_CONF = True
|
||||
|
||||
bq76972_ns = cg.esphome_ns.namespace("bq76972")
|
||||
BQ76972Component = bq76972_ns.class_(
|
||||
"BQ76972Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONF_REG_DISABLED = "reg_disabled"
|
||||
CONF_CRC_ENABLED = "crc_enabled"
|
||||
CONF_STACK_VOLTAGE = "stack_voltage"
|
||||
|
||||
# Ordered as the chip reports them, starting at command 0x68
|
||||
CONF_TEMPERATURES = [
|
||||
"internal_temperature",
|
||||
"cfetoff_temperature",
|
||||
"dfetoff_temperature",
|
||||
"alert_temperature",
|
||||
"ts1_temperature",
|
||||
"ts2_temperature",
|
||||
"ts3_temperature",
|
||||
"hdq_temperature",
|
||||
"dchg_temperature",
|
||||
"ddsg_temperature",
|
||||
]
|
||||
|
||||
CONF_CELL_VOLTAGES = [f"cell_{i}_voltage" for i in range(1, 17)]
|
||||
|
||||
voltage_sensor_schema = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
temp_sensor_schema = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BQ76972Component),
|
||||
cv.Optional(CONF_CRC_ENABLED, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REG_DISABLED, default=False): cv.boolean,
|
||||
cv.Optional(CONF_STACK_VOLTAGE): voltage_sensor_schema,
|
||||
**{cv.Optional(key): voltage_sensor_schema for key in CONF_CELL_VOLTAGES},
|
||||
**{cv.Optional(key): temp_sensor_schema for key in CONF_TEMPERATURES},
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x08))
|
||||
.extend(cv.polling_component_schema("10s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_crc_mode(config[CONF_CRC_ENABLED]))
|
||||
cg.add(var.set_reg_disable(config[CONF_REG_DISABLED]))
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_STACK_VOLTAGE, var.set_stack_voltage_sensor)
|
||||
|
||||
for idx, key in enumerate(CONF_TEMPERATURES):
|
||||
if (conf := config.get(key)) is not None:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_temperature_sensor(sens, idx))
|
||||
|
||||
for idx, key in enumerate(CONF_CELL_VOLTAGES):
|
||||
if (conf := config.get(key)) is not None:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_cell_sensor(sens, idx))
|
||||
|
||||
|
||||
BQ76972_PROGRAM_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BQ76972Component),
|
||||
cv.Required(CONF_ADDRESS): cv.templatable(cv.i2c_address),
|
||||
},
|
||||
key=CONF_ADDRESS,
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"bq76972.program_i2c_address",
|
||||
BQ76972_PROGRAM_I2C_ADDRESS_SCHEMA,
|
||||
automation.ApplyField(CONF_ADDRESS, "program_i2c_address", cg.uint8),
|
||||
)
|
||||
@@ -1,298 +0,0 @@
|
||||
#include "bq76972.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bq76972 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bq76972");
|
||||
|
||||
static constexpr uint8_t BQ76972_REG_SUBCOMMAND = 0x3E; // 0x3E low byte, 0x3F high byte
|
||||
static constexpr uint8_t BQ76972_REG_TRANSFER_BUFFER = 0x40;
|
||||
static constexpr uint8_t BQ76972_REG_CHECKSUM = 0x60; // 0x60 checksum, 0x61 length
|
||||
static constexpr uint8_t BQ76972_CMD_BAT_STATUS = 0x12;
|
||||
static constexpr uint8_t BQ76972_CMD_CELL_START = 0x14;
|
||||
static constexpr uint8_t BQ76972_CMD_STACK_VOLTAGE = 0x34;
|
||||
static constexpr uint8_t BQ76972_CMD_TEMP_START = 0x68;
|
||||
static constexpr uint16_t BQ76972_SUBCMD_ENTER_CFG_UPDATE = 0x0090;
|
||||
static constexpr uint16_t BQ76972_SUBCMD_EXIT_CFG_UPDATE = 0x0092;
|
||||
static constexpr uint16_t BQ76972_MEM_REG12_CONFIG = 0x9236;
|
||||
static constexpr uint16_t BQ76972_MEM_REG0_CONFIG = 0x9237;
|
||||
static constexpr uint16_t BQ76972_MEM_I2C_ADDRESS = 0x923A;
|
||||
static constexpr uint16_t BQ76972_MEM_SWAP_COMM_MODE = 0x29BC;
|
||||
// Pin config of temperature reading N (1 = CFETOFF ... 9 = DDSG) is at this base + N
|
||||
static constexpr uint16_t BQ76972_MEM_TEMP_PIN_CONFIG_BASE = 0x92F9;
|
||||
static constexpr size_t BQ76972_TS1_INDEX = 4; // a thermistor input by default
|
||||
static constexpr uint8_t BQ76972_PIN_CONFIG_THERMISTOR = 0x07;
|
||||
|
||||
// 16 words with a CRC byte after every data byte
|
||||
static constexpr size_t BQ76972_MAX_TRANSFER = 64;
|
||||
static constexpr uint8_t BQ76972_MAX_POLLS = 50;
|
||||
|
||||
static float raw_to_celsius(uint16_t raw) { return static_cast<int16_t>(raw) * 0.1f - 273.15f; }
|
||||
|
||||
bool BQ76972Component::read_block_(uint8_t start_register, uint8_t *data, size_t len) {
|
||||
if (!this->crc_mode_)
|
||||
return this->read_bytes(start_register, data, len);
|
||||
|
||||
// With CRC enabled, every data byte is followed by a CRC byte
|
||||
std::array<uint8_t, BQ76972_MAX_TRANSFER> buffer;
|
||||
if (len == 0 || len * 2 > buffer.size())
|
||||
return false;
|
||||
if (!this->read_bytes(start_register, buffer.data(), len * 2))
|
||||
return false;
|
||||
|
||||
// The first CRC also covers the write address, register and read address
|
||||
const uint8_t crc0_input[4] = {static_cast<uint8_t>(this->address_ << 1), start_register,
|
||||
static_cast<uint8_t>((this->address_ << 1) | 1), buffer[0]};
|
||||
if (crc8(crc0_input, 4, 0x00, 0x07, true) != buffer[1]) {
|
||||
ESP_LOGD(TAG, "CRC mismatch at register 0x%02X byte 0", start_register);
|
||||
return false;
|
||||
}
|
||||
data[0] = buffer[0];
|
||||
for (size_t i = 1; i < len; ++i) {
|
||||
if (crc8(&buffer[i * 2], 1, 0x00, 0x07, true) != buffer[i * 2 + 1]) {
|
||||
ESP_LOGD(TAG, "CRC mismatch at register 0x%02X byte %u", start_register, static_cast<unsigned>(i));
|
||||
return false;
|
||||
}
|
||||
data[i] = buffer[i * 2];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BQ76972Component::write_block_(uint8_t start_register, const uint8_t *data, size_t len) {
|
||||
if (!this->crc_mode_)
|
||||
return this->write_bytes(start_register, data, len);
|
||||
|
||||
std::array<uint8_t, BQ76972_MAX_TRANSFER> buffer;
|
||||
if (len == 0 || len * 2 > buffer.size())
|
||||
return false;
|
||||
|
||||
// The first CRC also covers the write address and register
|
||||
const uint8_t crc0_input[3] = {static_cast<uint8_t>(this->address_ << 1), start_register, data[0]};
|
||||
buffer[0] = data[0];
|
||||
buffer[1] = crc8(crc0_input, 3, 0x00, 0x07, true);
|
||||
for (size_t i = 1; i < len; ++i) {
|
||||
buffer[i * 2] = data[i];
|
||||
buffer[i * 2 + 1] = crc8(&data[i], 1, 0x00, 0x07, true);
|
||||
}
|
||||
return this->write_bytes(start_register, buffer.data(), len * 2);
|
||||
}
|
||||
|
||||
bool BQ76972Component::read_words_(uint8_t start_register, uint16_t *data, size_t num_words) {
|
||||
std::array<uint8_t, BQ76972_MAX_TRANSFER / 2> bytes;
|
||||
if (num_words * 2 > bytes.size() || !this->read_block_(start_register, bytes.data(), num_words * 2))
|
||||
return false;
|
||||
for (size_t i = 0; i < num_words; i++)
|
||||
data[i] = encode_uint16(bytes[2 * i + 1], bytes[2 * i]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BQ76972Component::send_subcommand_(uint16_t subcommand) {
|
||||
const uint8_t bytes[2] = {static_cast<uint8_t>(subcommand & 0xFF), static_cast<uint8_t>(subcommand >> 8)};
|
||||
return this->write_block_(BQ76972_REG_SUBCOMMAND, bytes, 2);
|
||||
}
|
||||
|
||||
bool BQ76972Component::wait_for_subcommand_() {
|
||||
uint8_t bytes[2];
|
||||
for (uint8_t i = 0; i < BQ76972_MAX_POLLS; i++) {
|
||||
if (!this->read_block_(BQ76972_REG_SUBCOMMAND, bytes, 2))
|
||||
return false;
|
||||
if (bytes[0] != 0xFF && bytes[1] != 0xFF)
|
||||
return true;
|
||||
delay(1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BQ76972Component::wait_for_cfgupdate_() {
|
||||
uint16_t status;
|
||||
for (uint8_t i = 0; i < BQ76972_MAX_POLLS; i++) {
|
||||
if (!this->read_words_(BQ76972_CMD_BAT_STATUS, &status, 1))
|
||||
return false;
|
||||
if (status & 0x0001)
|
||||
return true;
|
||||
delay(1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BQ76972Component::read_subcommand_(uint16_t subcommand, uint8_t *data, size_t len) {
|
||||
return this->send_subcommand_(subcommand) && this->wait_for_subcommand_() &&
|
||||
this->read_block_(BQ76972_REG_TRANSFER_BUFFER, data, len);
|
||||
}
|
||||
|
||||
bool BQ76972Component::write_subcommand_(uint16_t subcommand, const uint8_t *data, size_t len) {
|
||||
if (!this->send_subcommand_(subcommand))
|
||||
return false;
|
||||
if (len == 0)
|
||||
return true;
|
||||
if (!this->write_block_(BQ76972_REG_TRANSFER_BUFFER, data, len))
|
||||
return false;
|
||||
|
||||
// Inverted 8-bit sum of 0x3E, 0x3F and the data; length counts 0x3E, 0x3F, 0x60, 0x61 and the data
|
||||
uint8_t sum = static_cast<uint8_t>(subcommand & 0xFF) + static_cast<uint8_t>(subcommand >> 8);
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
sum += data[i];
|
||||
const uint8_t tail[2] = {static_cast<uint8_t>(~sum), static_cast<uint8_t>(4 + len)};
|
||||
return this->write_block_(BQ76972_REG_CHECKSUM, tail, 2);
|
||||
}
|
||||
|
||||
bool BQ76972Component::enter_cfg_update_() {
|
||||
if (!this->write_subcommand_(BQ76972_SUBCMD_ENTER_CFG_UPDATE, nullptr, 0))
|
||||
return false;
|
||||
delay(10);
|
||||
return this->wait_for_cfgupdate_();
|
||||
}
|
||||
|
||||
bool BQ76972Component::exit_cfg_update_() {
|
||||
return this->write_subcommand_(BQ76972_SUBCMD_EXIT_CFG_UPDATE, nullptr, 0);
|
||||
}
|
||||
|
||||
bool BQ76972Component::regs_disabled_(bool &out) {
|
||||
uint8_t reg12;
|
||||
uint8_t reg0;
|
||||
if (!this->read_subcommand_(BQ76972_MEM_REG12_CONFIG, ®12, 1) ||
|
||||
!this->read_subcommand_(BQ76972_MEM_REG0_CONFIG, ®0, 1))
|
||||
return false;
|
||||
out = reg12 == 0 && (reg0 & 0x01) == 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
const LogString *BQ76972Component::configure_() {
|
||||
uint16_t internal_temp;
|
||||
if (!this->read_words_(BQ76972_CMD_TEMP_START, &internal_temp, 1))
|
||||
return LOG_STR(ESP_LOG_MSG_COMM_FAIL);
|
||||
|
||||
// Only write the regulator config when it differs, see
|
||||
// https://e2e.ti.com/support/power-management-group/power-management/f/power-management-forum/1583718/bq76972-undocumented-4-minute-config_update-auto-exit-timeout
|
||||
bool regs_disabled = true;
|
||||
if (this->reg_disable_ && !this->regs_disabled_(regs_disabled))
|
||||
return LOG_STR("Reading regulator config failed");
|
||||
const bool write_regs = !regs_disabled;
|
||||
|
||||
bool write_thermistors = false;
|
||||
for (size_t i = 1; i < BQ76972_NUM_TEMPERATURES; i++) {
|
||||
if (i != BQ76972_TS1_INDEX && this->temperature_sensors_[i] != nullptr)
|
||||
write_thermistors = true;
|
||||
}
|
||||
|
||||
if (write_regs || write_thermistors) {
|
||||
if (!this->enter_cfg_update_())
|
||||
return LOG_STR("Entering config update mode failed");
|
||||
const LogString *error = this->write_config_(write_regs);
|
||||
// The chip stops measuring in config update mode, so leave it even after a failed write
|
||||
if (!this->exit_cfg_update_() && error == nullptr)
|
||||
error = LOG_STR("Exiting config update mode failed");
|
||||
if (error != nullptr)
|
||||
return error;
|
||||
if (write_regs && (!this->regs_disabled_(regs_disabled) || !regs_disabled))
|
||||
return LOG_STR("Disabling regulators failed");
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Internal temperature: %.1f°C", raw_to_celsius(internal_temp));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const LogString *BQ76972Component::write_config_(bool write_regs) {
|
||||
if (write_regs) {
|
||||
const uint8_t zero = 0;
|
||||
if (!this->write_subcommand_(BQ76972_MEM_REG12_CONFIG, &zero, 1) ||
|
||||
!this->write_subcommand_(BQ76972_MEM_REG0_CONFIG, &zero, 1))
|
||||
return LOG_STR("Writing regulator config failed");
|
||||
}
|
||||
for (size_t i = 1; i < BQ76972_NUM_TEMPERATURES; i++) {
|
||||
if (i == BQ76972_TS1_INDEX || this->temperature_sensors_[i] == nullptr)
|
||||
continue;
|
||||
if (!this->write_subcommand_(BQ76972_MEM_TEMP_PIN_CONFIG_BASE + i, &BQ76972_PIN_CONFIG_THERMISTOR, 1))
|
||||
return LOG_STR("Writing thermistor pin config failed");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void BQ76972Component::setup() {
|
||||
if (const LogString *error = this->configure_(); error != nullptr)
|
||||
this->mark_failed(error);
|
||||
}
|
||||
|
||||
void BQ76972Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BQ76972:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" CRC Mode: %s\n"
|
||||
" Regulators disabled: %s",
|
||||
this->crc_mode_ ? LOG_STR_LITERAL("Enabled") : LOG_STR_LITERAL("Disabled"), YESNO(this->reg_disable_));
|
||||
}
|
||||
|
||||
void BQ76972Component::program_i2c_address(uint8_t address) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
const uint8_t shifted = address << 1;
|
||||
if (!this->enter_cfg_update_()) {
|
||||
this->status_set_warning(LOG_STR("Programming I2C address failed"));
|
||||
return;
|
||||
}
|
||||
// The new address only takes effect with the swap, so after a failed write the chip still answers at the
|
||||
// old one and must still be taken out of config update mode
|
||||
const bool written = this->write_subcommand_(BQ76972_MEM_I2C_ADDRESS, &shifted, 1) &&
|
||||
this->write_subcommand_(BQ76972_MEM_SWAP_COMM_MODE, nullptr, 0);
|
||||
if (written) {
|
||||
delay(10);
|
||||
this->set_i2c_address(address);
|
||||
}
|
||||
if (!this->exit_cfg_update_() || !written) {
|
||||
this->status_set_warning(LOG_STR("Programming I2C address failed"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "I2C address set to 0x%02X", address);
|
||||
}
|
||||
|
||||
template<size_t N> static bool any_set(const std::array<sensor::Sensor *, N> &sensors) {
|
||||
for (auto *sensor : sensors) {
|
||||
if (sensor != nullptr)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BQ76972Component::update() {
|
||||
// Each group publishes as soon as its read succeeds, so one failed read does not hold back the others
|
||||
bool ok = true;
|
||||
uint16_t words[BQ76972_NUM_CELLS];
|
||||
if (this->stack_voltage_sensor_ != nullptr) {
|
||||
if (this->read_words_(BQ76972_CMD_STACK_VOLTAGE, words, 1)) {
|
||||
if (words[0] != 0)
|
||||
this->stack_voltage_sensor_->publish_state(words[0] * 0.01f); // 10 mV units
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (any_set(this->cell_sensors_)) {
|
||||
if (this->read_words_(BQ76972_CMD_CELL_START, words, BQ76972_NUM_CELLS)) {
|
||||
for (size_t i = 0; i < BQ76972_NUM_CELLS; i++) {
|
||||
if (this->cell_sensors_[i] != nullptr && words[i] != 0)
|
||||
this->cell_sensors_[i]->publish_state(words[i] * 0.001f); // mV
|
||||
}
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (any_set(this->temperature_sensors_)) {
|
||||
if (this->read_words_(BQ76972_CMD_TEMP_START, words, BQ76972_NUM_TEMPERATURES)) {
|
||||
for (size_t i = 0; i < BQ76972_NUM_TEMPERATURES; i++) {
|
||||
if (this->temperature_sensors_[i] != nullptr)
|
||||
this->temperature_sensors_[i]->publish_state(raw_to_celsius(words[i]));
|
||||
}
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::bq76972
|
||||
@@ -1,51 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome::bq76972 {
|
||||
|
||||
static constexpr size_t BQ76972_NUM_CELLS = 16;
|
||||
static constexpr size_t BQ76972_NUM_TEMPERATURES = 10;
|
||||
|
||||
class BQ76972Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_crc_mode(bool crc_mode) { this->crc_mode_ = crc_mode; }
|
||||
void set_reg_disable(bool reg_disable) { this->reg_disable_ = reg_disable; }
|
||||
void set_stack_voltage_sensor(sensor::Sensor *s) { this->stack_voltage_sensor_ = s; }
|
||||
void set_cell_sensor(sensor::Sensor *s, size_t index) { this->cell_sensors_[index] = s; }
|
||||
void set_temperature_sensor(sensor::Sensor *s, size_t index) { this->temperature_sensors_[index] = s; }
|
||||
|
||||
void program_i2c_address(uint8_t address);
|
||||
|
||||
protected:
|
||||
bool read_block_(uint8_t start_register, uint8_t *data, size_t len);
|
||||
bool write_block_(uint8_t start_register, const uint8_t *data, size_t len);
|
||||
bool read_words_(uint8_t start_register, uint16_t *data, size_t num_words);
|
||||
bool send_subcommand_(uint16_t subcommand);
|
||||
bool read_subcommand_(uint16_t subcommand, uint8_t *data, size_t len);
|
||||
bool write_subcommand_(uint16_t subcommand, const uint8_t *data, size_t len);
|
||||
bool wait_for_subcommand_();
|
||||
bool wait_for_cfgupdate_();
|
||||
bool enter_cfg_update_();
|
||||
bool exit_cfg_update_();
|
||||
bool regs_disabled_(bool &out);
|
||||
const LogString *configure_();
|
||||
const LogString *write_config_(bool write_regs);
|
||||
|
||||
bool reg_disable_{false};
|
||||
bool crc_mode_{false};
|
||||
|
||||
sensor::Sensor *stack_voltage_sensor_{nullptr};
|
||||
std::array<sensor::Sensor *, BQ76972_NUM_CELLS> cell_sensors_{};
|
||||
std::array<sensor::Sensor *, BQ76972_NUM_TEMPERATURES> temperature_sensors_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::bq76972
|
||||
@@ -1,17 +1,14 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_ID, CONF_PLATFORM, CONF_TRIGGER_ID
|
||||
from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@mvturnho", "@danielschramm"]
|
||||
DOMAIN = "canbus"
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -23,9 +20,6 @@ CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request"
|
||||
CONF_CANBUS_ID = "canbus_id"
|
||||
CONF_BIT_RATE = "bit_rate"
|
||||
CONF_ON_FRAME = "on_frame"
|
||||
CONF_MAX_FRAMES_PER_LOOP = "max_frames_per_loop"
|
||||
DEFAULT_MAX_FRAMES_PER_LOOP = 50 # Keep in sync with max_frames_per_loop_ in canbus.h
|
||||
DEFAULT_BIT_RATE = "125KBPS" # Keep in sync with bit_rate_ in canbus.h
|
||||
|
||||
|
||||
def validate_id(config: ConfigType) -> ConfigType:
|
||||
@@ -93,13 +87,8 @@ CANBUS_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CanbusComponent),
|
||||
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
|
||||
cv.Optional(CONF_BIT_RATE, default=DEFAULT_BIT_RATE): cv.enum(
|
||||
CAN_SPEEDS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_BIT_RATE, default="125KBPS"): cv.enum(CAN_SPEEDS, upper=True),
|
||||
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_MAX_FRAMES_PER_LOOP, default=DEFAULT_MAX_FRAMES_PER_LOOP
|
||||
): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_ON_FRAME): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger),
|
||||
@@ -120,13 +109,9 @@ CANBUS_SCHEMA.add_extra(validate_id)
|
||||
|
||||
async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_can_id(config[CONF_CAN_ID]))
|
||||
if config[CONF_USE_EXTENDED_ID]:
|
||||
cg.add(var.set_use_extended_id(True))
|
||||
if (bit_rate := config[CONF_BIT_RATE]) != DEFAULT_BIT_RATE:
|
||||
cg.add(var.set_bitrate(CAN_SPEEDS[bit_rate]))
|
||||
if (max_frames := config[CONF_MAX_FRAMES_PER_LOOP]) != DEFAULT_MAX_FRAMES_PER_LOOP:
|
||||
cg.add(var.set_max_frames_per_loop(max_frames))
|
||||
cg.add(var.set_can_id([config[CONF_CAN_ID]]))
|
||||
cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]]))
|
||||
cg.add(var.set_bitrate(CAN_SPEEDS[config[CONF_BIT_RATE]]))
|
||||
|
||||
for conf in config.get(CONF_ON_FRAME, []):
|
||||
can_id = conf[CONF_CAN_ID]
|
||||
@@ -153,19 +138,9 @@ async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def register_canbus(
|
||||
var: MockObj, config: ConfigType, *, reports_status: bool | None = None
|
||||
) -> None:
|
||||
async def register_canbus(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.new_Pvariable(config[CONF_ID], var)
|
||||
if reports_status is None:
|
||||
_LOGGER.warning(
|
||||
"CAN bus platform '%s' does not report bus state, so bus-off recovery and "
|
||||
"error-state logging are disabled. Platform authors should either override "
|
||||
"get_status() and get_events() and call register_canbus(..., reports_status=True) "
|
||||
"or pass reports_status=False if the hardware cannot report it",
|
||||
config[CONF_PLATFORM],
|
||||
)
|
||||
await setup_canbus_core_(var, config)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "canbus.h"
|
||||
#include <algorithm>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::canbus {
|
||||
@@ -66,25 +65,17 @@ void Canbus::add_trigger(CanbusTrigger *trigger) {
|
||||
};
|
||||
|
||||
void Canbus::loop() {
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
if ((now - this->last_event_check_time_) >= EVENT_CHECK_INTERVAL_MS) {
|
||||
this->log_events_(this->get_events(), now);
|
||||
this->last_event_check_time_ = now;
|
||||
}
|
||||
|
||||
struct CanFrame can_message;
|
||||
uint32_t message_counter = 0;
|
||||
for (; message_counter < this->max_frames_per_loop_; message_counter++) {
|
||||
if (this->read_message(&can_message) != canbus::ERROR_OK) {
|
||||
break;
|
||||
}
|
||||
|
||||
// read all messages until queue is empty
|
||||
int message_counter = 0;
|
||||
while (this->read_message(&can_message) == canbus::ERROR_OK) {
|
||||
message_counter++;
|
||||
if (can_message.use_extended_id) {
|
||||
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") extended can_id=0x%" PRIx32 " size=%d", message_counter + 1,
|
||||
ESP_LOGD(TAG, "received can message (#%d) extended can_id=0x%" PRIx32 " size=%d", message_counter,
|
||||
can_message.can_id, can_message.can_data_length_code);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") std can_id=0x%" PRIx32 " size=%d", message_counter + 1,
|
||||
can_message.can_id, can_message.can_data_length_code);
|
||||
ESP_LOGD(TAG, "received can message (#%d) std can_id=0x%" PRIx32 " size=%d", message_counter, can_message.can_id,
|
||||
can_message.can_data_length_code);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
@@ -108,63 +99,6 @@ void Canbus::loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message_counter == this->max_frames_per_loop_) {
|
||||
ESP_LOGW(TAG,
|
||||
"Reached max_frames_per_loop=%" PRIu32 ", deferring remaining CAN frames. Some frames might be dropped",
|
||||
this->max_frames_per_loop_);
|
||||
}
|
||||
}
|
||||
|
||||
void Canbus::log_events_(CanEventFlags events, uint32_t now) {
|
||||
// Bus-off gets its own handling because automatic recovery can make it flap
|
||||
if (events & CanEventFlags::CAN_EVENT_BUS_OFF) {
|
||||
this->last_bus_off_time_ = now;
|
||||
if (!this->bus_off_) {
|
||||
this->bus_off_ = true;
|
||||
this->status_set_warning(LOG_STR("Bus off"));
|
||||
}
|
||||
} else if (this->bus_off_) {
|
||||
if ((now - this->last_bus_off_time_) >= EVENT_LOG_BUS_OFF_HOLDOFF_MS) {
|
||||
// No bus-off event for the hold-off time, so ask whether it is really over
|
||||
if (this->get_status().bus_off) {
|
||||
this->last_bus_off_time_ = now;
|
||||
} else {
|
||||
this->bus_off_ = false;
|
||||
this->status_clear_warning();
|
||||
// Events gathered while bus-off say nothing about the recovered bus
|
||||
this->events_to_log_ = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->events_to_log_ |= events;
|
||||
if (this->events_to_log_ != 0 && (now - this->last_event_log_time_) >= EVENT_LOG_THROTTLE_MS) {
|
||||
if (this->events_to_log_ & (CanEventFlags::CAN_EVENT_PASSIVE | CanEventFlags::CAN_EVENT_ACTIVE)) {
|
||||
bool error_passive = this->events_to_log_ & CanEventFlags::CAN_EVENT_PASSIVE;
|
||||
if (error_passive && (this->events_to_log_ & CanEventFlags::CAN_EVENT_ACTIVE)) {
|
||||
// Both edges within one throttle window, so the counters decide the current state
|
||||
auto status = this->get_status();
|
||||
error_passive = status.rx_error_counter >= CAN_ERROR_PASSIVE_THRESHOLD ||
|
||||
status.tx_error_counter >= CAN_ERROR_PASSIVE_THRESHOLD;
|
||||
}
|
||||
ESP_LOGW(TAG, "entered %s", error_passive ? LOG_STR_LITERAL("error-passive") : LOG_STR_LITERAL("error-active"));
|
||||
}
|
||||
if (this->events_to_log_ & CanEventFlags::CAN_EVENT_RX_QUEUE_FULL) {
|
||||
ESP_LOGD(TAG, "receive buffer overrun");
|
||||
}
|
||||
this->events_to_log_ = 0;
|
||||
this->last_event_log_time_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
if ((now - this->last_state_log_time_) >= STATE_LOG_INTERVAL_MS) {
|
||||
auto status = this->get_status();
|
||||
ESP_LOGV(TAG, "Status: bus_off %d, tx_err %d, rx_err %d", status.bus_off, status.tx_error_counter,
|
||||
status.rx_error_counter);
|
||||
this->last_state_log_time_ = now;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::canbus
|
||||
|
||||
@@ -48,8 +48,6 @@ template<typename... Ts> class CanbusSendAction;
|
||||
/* CAN payload length definitions according to ISO 11898-1 */
|
||||
static const uint8_t CAN_MAX_DATA_LENGTH = 8;
|
||||
|
||||
static const uint8_t CAN_ERROR_PASSIVE_THRESHOLD = 128;
|
||||
|
||||
/*
|
||||
Can Frame describes a normative CAN Frame
|
||||
The RTR = Remote Transmission Request is implemented in every CAN controller but rarely used
|
||||
@@ -63,26 +61,6 @@ struct CanFrame {
|
||||
uint8_t data[CAN_MAX_DATA_LENGTH] __attribute__((aligned(8)));
|
||||
};
|
||||
|
||||
enum CanEventFlags : uint8_t {
|
||||
CAN_EVENT_PASSIVE = 1 << 0,
|
||||
CAN_EVENT_ACTIVE = 1 << 1,
|
||||
CAN_EVENT_BUS_OFF = 1 << 2,
|
||||
CAN_EVENT_RX_QUEUE_FULL = 1 << 3,
|
||||
};
|
||||
|
||||
static constexpr uint32_t CAN_STATUS_UNSET = UINT32_MAX;
|
||||
|
||||
struct CanStatus {
|
||||
bool bus_off{false};
|
||||
uint8_t rx_error_counter{0};
|
||||
uint8_t tx_error_counter{0};
|
||||
uint32_t tx_failed_count{CAN_STATUS_UNSET};
|
||||
uint32_t rx_missed_count{CAN_STATUS_UNSET};
|
||||
uint32_t rx_overrun_count{CAN_STATUS_UNSET};
|
||||
uint32_t arb_lost_count{CAN_STATUS_UNSET};
|
||||
uint32_t bus_error_count{CAN_STATUS_UNSET};
|
||||
};
|
||||
|
||||
class Canbus : public Component {
|
||||
public:
|
||||
Canbus(){};
|
||||
@@ -104,8 +82,6 @@ class Canbus : public Component {
|
||||
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
|
||||
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
|
||||
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
|
||||
void set_max_frames_per_loop(uint32_t max_frames_per_loop) { this->max_frames_per_loop_ = max_frames_per_loop; }
|
||||
virtual CanStatus get_status() { return {}; }
|
||||
|
||||
void add_trigger(CanbusTrigger *trigger);
|
||||
/**
|
||||
@@ -124,36 +100,14 @@ class Canbus : public Component {
|
||||
template<typename... Ts> friend class CanbusSendAction;
|
||||
std::vector<CanbusTrigger *> triggers_{};
|
||||
uint32_t can_id_;
|
||||
uint32_t max_frames_per_loop_{50}; // Keep in sync with DEFAULT_MAX_FRAMES_PER_LOOP in __init__.py
|
||||
CanSpeed bit_rate_{CAN_125KBPS}; // Keep in sync with DEFAULT_BIT_RATE in __init__.py
|
||||
bool use_extended_id_{false};
|
||||
uint8_t events_to_log_{0};
|
||||
bool bus_off_{false};
|
||||
bool use_extended_id_;
|
||||
CanSpeed bit_rate_;
|
||||
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
|
||||
callback_manager_{};
|
||||
|
||||
// interval in which to check for new can bus events (bus-off, passive, active)
|
||||
static constexpr uint32_t EVENT_CHECK_INTERVAL_MS = 100;
|
||||
uint32_t last_event_check_time_{0};
|
||||
|
||||
// time to wait after logging an event before logging the next event(s)
|
||||
static constexpr uint32_t EVENT_LOG_THROTTLE_MS = 1000;
|
||||
uint32_t last_event_log_time_{0};
|
||||
|
||||
// time span without bus-off events before the bus is declared recovered
|
||||
static constexpr uint32_t EVENT_LOG_BUS_OFF_HOLDOFF_MS = 1000;
|
||||
uint32_t last_bus_off_time_{0};
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
static constexpr uint32_t STATE_LOG_INTERVAL_MS = 1000;
|
||||
uint32_t last_state_log_time_{0};
|
||||
#endif
|
||||
|
||||
void log_events_(CanEventFlags events, uint32_t now);
|
||||
virtual bool setup_internal() = 0;
|
||||
virtual Error send_message(struct CanFrame *frame) = 0;
|
||||
virtual Error read_message(struct CanFrame *frame) = 0;
|
||||
virtual CanEventFlags get_events() { return CanEventFlags{}; }
|
||||
};
|
||||
|
||||
template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, public Parented<Canbus> {
|
||||
|
||||
@@ -4,6 +4,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import web_server_base, wifi
|
||||
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
|
||||
from esphome.components.wifi import DOMAIN as WIFI_DOMAIN
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AP,
|
||||
@@ -15,6 +16,7 @@ from esphome.const import (
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RP2,
|
||||
PLATFORM_RTL87XX,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
@@ -76,7 +78,17 @@ def _final_validate(config: ConfigType) -> None:
|
||||
"Add 'ap:' to your WiFi configuration to enable the captive portal."
|
||||
)
|
||||
|
||||
web_server_base.consume_captive_dns_sockets(config, DOMAIN)
|
||||
# Register socket needs for DNS server and additional HTTP connections
|
||||
# - 1 UDP socket for DNS server
|
||||
# - 3 TCP sockets for captive portal detection probes + configuration requests
|
||||
# OS captive portal detection makes multiple probe requests that stay in TIME_WAIT.
|
||||
# Need headroom for actual user configuration requests.
|
||||
# LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts.
|
||||
# The listening socket is registered by web_server_base (shared HTTP server).
|
||||
from esphome.components import socket
|
||||
|
||||
socket.consume_sockets(3, "captive_portal")(config)
|
||||
socket.consume_sockets(1, "captive_portal", socket.SocketType.UDP)(config)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
@@ -96,4 +108,16 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config[CONF_COMPRESSION] == "gzip":
|
||||
cg.add_define("USE_CAPTIVE_PORTAL_GZIP")
|
||||
|
||||
web_server_base.add_captive_dns_library()
|
||||
if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2):
|
||||
cg.add_library("DNSServer", None)
|
||||
|
||||
|
||||
# Only compile the ESP-IDF DNS server when using ESP-IDF framework
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
{
|
||||
"dns_server_esp32_idf.cpp": {
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/components/wifi/scan_list.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#ifdef USE_PROVISIONING
|
||||
#include "esphome/components/provisioning/provisioning.h"
|
||||
#endif
|
||||
#include "captive_index.h"
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
@@ -78,6 +81,20 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
void CaptivePortal::setup() {
|
||||
// Disable loop by default - will be enabled when captive portal starts
|
||||
this->disable_loop();
|
||||
#ifdef USE_PROVISIONING
|
||||
// The captive portal is a provisioning surface: once the provisioning window
|
||||
// has closed, stop serving it. WiFi's own closed-callback shuts down the
|
||||
// access point the portal runs on, and the gated fallback in WiFiComponent's
|
||||
// loop() ensures neither is started again afterwards.
|
||||
if (provisioning::global_provisioning_manager != nullptr) {
|
||||
provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
|
||||
if (this->active_) {
|
||||
ESP_LOGD(TAG, "Provisioning window closed; stopping captive portal");
|
||||
this->end();
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
void CaptivePortal::start() {
|
||||
this->base_->init();
|
||||
@@ -85,7 +102,17 @@ void CaptivePortal::start() {
|
||||
this->base_->add_handler_without_auth(this);
|
||||
}
|
||||
|
||||
this->dns_.start(wifi::global_wifi_component->wifi_soft_ap_ip());
|
||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||
|
||||
#if defined(USE_ESP32)
|
||||
// Create DNS server instance for ESP-IDF
|
||||
this->dns_server_ = make_unique<DNSServer>();
|
||||
this->dns_server_->start(ip);
|
||||
#elif defined(USE_ARDUINO)
|
||||
this->dns_server_ = make_unique<DNSServer>();
|
||||
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
|
||||
this->dns_server_->start(53, ESPHOME_F("*"), ip);
|
||||
#endif
|
||||
|
||||
this->initialized_ = true;
|
||||
this->active_ = true;
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
#include <memory>
|
||||
#if defined(USE_ESP32)
|
||||
#include "dns_server_esp32_idf.h"
|
||||
#elif defined(USE_ARDUINO)
|
||||
#include <DNSServer.h>
|
||||
#endif
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/components/web_server_base/web_server_base.h"
|
||||
#include "esphome/components/web_server_base/captive_dns.h"
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
@@ -14,7 +19,17 @@ class CaptivePortal final : public AsyncWebHandler, public Component {
|
||||
CaptivePortal(web_server_base::WebServerBase *base);
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override { this->dns_.loop(); }
|
||||
void loop() override {
|
||||
#if defined(USE_ESP32)
|
||||
if (this->dns_server_ != nullptr) {
|
||||
this->dns_server_->process_next_request();
|
||||
}
|
||||
#elif defined(USE_ARDUINO)
|
||||
if (this->dns_server_ != nullptr) {
|
||||
this->dns_server_->processNextRequest();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
float get_setup_priority() const override;
|
||||
void start();
|
||||
bool is_active() const { return this->active_; }
|
||||
@@ -22,7 +37,10 @@ class CaptivePortal final : public AsyncWebHandler, public Component {
|
||||
this->active_ = false;
|
||||
this->disable_loop(); // Stop processing DNS requests
|
||||
this->base_->deinit();
|
||||
this->dns_.stop();
|
||||
if (this->dns_server_ != nullptr) {
|
||||
this->dns_server_->stop();
|
||||
this->dns_server_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||
@@ -42,7 +60,9 @@ class CaptivePortal final : public AsyncWebHandler, public Component {
|
||||
web_server_base::WebServerBase *base_;
|
||||
bool initialized_{false};
|
||||
bool active_{false};
|
||||
web_server_base::CaptiveDNS dns_;
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
std::unique_ptr<DNSServer> dns_server_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
+8
-9
@@ -1,5 +1,5 @@
|
||||
#include "dns_server_esp32_idf.h"
|
||||
#if defined(USE_ESP32) && (defined(USE_CAPTIVE_PORTAL) || defined(USE_WEBSERVER_CAPTIVE))
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -7,9 +7,9 @@
|
||||
#include <lwip/sockets.h>
|
||||
#include <lwip/inet.h>
|
||||
|
||||
namespace esphome::web_server_base {
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "web_server_base.dns");
|
||||
ESPHOME_LOG_TAG(TAG, "captive_portal.dns");
|
||||
|
||||
// DNS constants
|
||||
static constexpr uint16_t DNS_PORT = 53;
|
||||
@@ -46,7 +46,7 @@ struct DNSAnswer {
|
||||
uint32_t ip_addr;
|
||||
} __attribute__((packed));
|
||||
|
||||
bool DNSServer::start(const network::IPAddress &ip) {
|
||||
void DNSServer::start(const network::IPAddress &ip) {
|
||||
this->server_ip_ = ip;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
@@ -57,7 +57,7 @@ bool DNSServer::start(const network::IPAddress &ip) {
|
||||
this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP).release();
|
||||
if (this->socket_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Socket create failed");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set socket options
|
||||
@@ -72,10 +72,9 @@ bool DNSServer::start(const network::IPAddress &ip) {
|
||||
if (err != 0) {
|
||||
ESP_LOGE(TAG, "Bind failed: %d", errno);
|
||||
this->destroy_socket_();
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Bound to port %d", DNS_PORT);
|
||||
return true;
|
||||
}
|
||||
|
||||
void DNSServer::stop() {
|
||||
@@ -203,6 +202,6 @@ void DNSServer::process_next_request() {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::web_server_base
|
||||
} // namespace esphome::captive_portal
|
||||
|
||||
#endif // USE_ESP32 && (USE_CAPTIVE_PORTAL || USE_WEBSERVER_CAPTIVE)
|
||||
#endif // USE_ESP32
|
||||
+5
-8
@@ -1,18 +1,15 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
// ESP32 backend for CaptiveDNS: answers every A query with the access point address.
|
||||
#if defined(USE_ESP32) && (defined(USE_CAPTIVE_PORTAL) || defined(USE_WEBSERVER_CAPTIVE))
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
|
||||
namespace esphome::web_server_base {
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
class DNSServer {
|
||||
public:
|
||||
/// Returns false when the socket could not be created or bound.
|
||||
bool start(const network::IPAddress &ip);
|
||||
void start(const network::IPAddress &ip);
|
||||
void stop();
|
||||
void process_next_request();
|
||||
|
||||
@@ -30,6 +27,6 @@ class DNSServer {
|
||||
uint8_t buffer_[DNS_BUFFER_SIZE];
|
||||
};
|
||||
|
||||
} // namespace esphome::web_server_base
|
||||
} // namespace esphome::captive_portal
|
||||
|
||||
#endif // USE_ESP32 && (USE_CAPTIVE_PORTAL || USE_WEBSERVER_CAPTIVE)
|
||||
#endif // USE_ESP32
|
||||
@@ -46,6 +46,7 @@ from esphome.const import (
|
||||
CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC,
|
||||
CONF_TARGET_TEMPERATURE_STATE_TOPIC,
|
||||
CONF_TEMPERATURE_STEP,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VISUAL,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
@@ -163,6 +164,14 @@ def visual_temperature_step(value: Any) -> ConfigType:
|
||||
)
|
||||
|
||||
|
||||
# Actions
|
||||
StateTrigger = climate_ns.class_(
|
||||
"StateTrigger", automation.Trigger.template(Climate.operator("ref"))
|
||||
)
|
||||
ControlTrigger = climate_ns.class_(
|
||||
"ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref"))
|
||||
)
|
||||
|
||||
_CLIMATE_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
@@ -241,8 +250,16 @@ _CLIMATE_SCHEMA = (
|
||||
cv.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.publish_topic
|
||||
),
|
||||
cv.Optional(CONF_ON_CONTROL): automation.validate_automation(),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(),
|
||||
cv.Optional(CONF_ON_CONTROL): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -271,16 +288,6 @@ def climate_schema(
|
||||
return _CLIMATE_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_STATE, "add_on_state_callback", [(Climate.operator("ref"), "x")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CONTROL, "add_on_control_callback", [(ClimateCall.operator("ref"), "x")]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("climate")
|
||||
async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
|
||||
visual = config.get(CONF_VISUAL, {})
|
||||
@@ -436,7 +443,17 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
|
||||
)
|
||||
)
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(Climate.operator("ref"), "x")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_CONTROL, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(ClimateCall.operator("ref"), "x")], conf
|
||||
)
|
||||
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "climate.h"
|
||||
|
||||
namespace esphome::climate {
|
||||
|
||||
class ControlTrigger final : public Trigger<ClimateCall &> {
|
||||
public:
|
||||
ControlTrigger(Climate *climate) {
|
||||
climate->add_on_control_callback([this](ClimateCall &x) { this->trigger(x); });
|
||||
}
|
||||
};
|
||||
|
||||
class StateTrigger final : public Trigger<Climate &> {
|
||||
public:
|
||||
StateTrigger(Climate *climate) {
|
||||
climate->add_on_state_callback([this](Climate &x) { this->trigger(x); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::climate
|
||||
@@ -6,16 +6,10 @@
|
||||
namespace esphome::cm1106 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cm1106");
|
||||
static constexpr uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
|
||||
static constexpr uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
|
||||
static constexpr uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
|
||||
// The factory default ABC calibration cycle differs between CM1106 models, so
|
||||
// every ABC write applies the configured cycle and baseline explicitly.
|
||||
static constexpr uint8_t C_M1106_CMD_SET_ABC_STATUS[10] = {0x11, 0x07, 0x10, 0x64, 0x00, 0x0F, 0x01, 0x90, 0x64, 0x00};
|
||||
static constexpr uint8_t C_M1106_CMD_SET_ABC_STATUS_RESPONSE[4] = {0x16, 0x01, 0x10, 0xD9};
|
||||
static const uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
|
||||
static const uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
|
||||
static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
|
||||
|
||||
static constexpr uint8_t CM1106_ABC_FLAG_ENABLE = 0x0;
|
||||
static constexpr uint8_t CM1106_ABC_FLAG_DISABLE = 0x2;
|
||||
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
|
||||
uint8_t crc = 0;
|
||||
for (size_t i = 0; i < len - 1; i++) {
|
||||
@@ -31,46 +25,34 @@ void CM1106Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->abc_boot_logic_ != CM1106_ABC_NONE) {
|
||||
this->abc_set_(this->abc_boot_logic_ == CM1106_ABC_ENABLED);
|
||||
}
|
||||
}
|
||||
|
||||
void CM1106Component::update() {
|
||||
uint8_t response[8] = {0};
|
||||
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
|
||||
ESP_LOGW(TAG, "Reading data failed!");
|
||||
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response[0] != 0x16 || response[1] != 0x05 || response[2] != 0x01) {
|
||||
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
|
||||
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
|
||||
response[3]);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t checksum = cm1106_checksum(response, sizeof(response));
|
||||
if (response[7] != checksum) {
|
||||
ESP_LOGW(TAG, "Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
|
||||
ESP_LOGW(TAG, "CM1106 Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
const uint16_t ppm = response[3] << 8 | response[4];
|
||||
const uint8_t status = response[5];
|
||||
if (status & 0x1) {
|
||||
ESP_LOGW(TAG, "Preheating; CO₂=%uppm", ppm);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "Status: %02X", status);
|
||||
}
|
||||
this->status_clear_warning();
|
||||
|
||||
ESP_LOGD(TAG, "CO₂=%uppm DF4=%02X", ppm, response[6]);
|
||||
uint16_t ppm = response[3] << 8 | response[4];
|
||||
ESP_LOGD(TAG, "CM1106 Received CO₂=%uppm DF3=%02X DF4=%02X", ppm, response[5], response[6]);
|
||||
if (this->co2_sensor_ != nullptr)
|
||||
this->co2_sensor_->publish_state(ppm);
|
||||
}
|
||||
@@ -80,38 +62,24 @@ void CM1106Component::calibrate_zero(uint16_t ppm) {
|
||||
memcpy(cmd, C_M1106_CMD_SET_CO2_CALIB, sizeof(cmd));
|
||||
cmd[3] = ppm >> 8;
|
||||
cmd[4] = ppm & 0xFF;
|
||||
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_CO2_CALIB_RESPONSE)) {
|
||||
ESP_LOGD(TAG, "Successfully calibrated sensor to %uppm", ppm);
|
||||
}
|
||||
}
|
||||
|
||||
void CM1106Component::abc_set_(bool enabled) {
|
||||
ESP_LOGD(TAG, "%sabling automatic baseline calibration", enabled ? LOG_STR_LITERAL("En") : LOG_STR_LITERAL("Dis"));
|
||||
uint8_t cmd[10];
|
||||
memcpy(cmd, C_M1106_CMD_SET_ABC_STATUS, sizeof(cmd));
|
||||
cmd[4] = enabled ? CM1106_ABC_FLAG_ENABLE : CM1106_ABC_FLAG_DISABLE;
|
||||
cmd[5] = this->abc_cycle_;
|
||||
cmd[6] = this->abc_baseline_ >> 8;
|
||||
cmd[7] = this->abc_baseline_ & 0xFF;
|
||||
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_ABC_STATUS_RESPONSE)) {
|
||||
ESP_LOGD(TAG, "Successfully set ABC status");
|
||||
}
|
||||
}
|
||||
|
||||
bool CM1106Component::send_command_expect_ack_(const uint8_t *command, size_t command_len, const uint8_t *ack) {
|
||||
uint8_t response[4] = {0};
|
||||
if (!this->cm1106_write_command_(command, command_len, response, sizeof(response))) {
|
||||
ESP_LOGW(TAG, "Command %02X failed", command[2]);
|
||||
|
||||
if (!this->cm1106_write_command_(cmd, sizeof(cmd), response, sizeof(response))) {
|
||||
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
if (memcmp(response, ack, sizeof(response)) != 0) {
|
||||
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
|
||||
|
||||
// check if correct response received
|
||||
if (memcmp(response, C_M1106_CMD_SET_CO2_CALIB_RESPONSE, sizeof(response)) != 0) {
|
||||
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
|
||||
response[3]);
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
return true;
|
||||
ESP_LOGD(TAG, "CM1106 Successfully calibrated sensor to %uppm", ppm);
|
||||
}
|
||||
|
||||
bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response,
|
||||
@@ -132,13 +100,6 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
|
||||
void CM1106Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CM1106:");
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
if (this->abc_boot_logic_ != CM1106_ABC_NONE) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Automatic baseline calibration on boot: %s\n"
|
||||
" ABC calibration cycle: %u days\n"
|
||||
" ABC baseline: %uppm",
|
||||
ONOFF(this->abc_boot_logic_ == CM1106_ABC_ENABLED), this->abc_cycle_, this->abc_baseline_);
|
||||
}
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
|
||||
@@ -6,14 +6,6 @@
|
||||
|
||||
namespace esphome::cm1106 {
|
||||
|
||||
// ABC logic requested at boot; CM1106_ABC_NONE means nothing is sent and the
|
||||
// sensor keeps its stored setting.
|
||||
enum CM1106ABCLogic : uint8_t {
|
||||
CM1106_ABC_NONE = 0,
|
||||
CM1106_ABC_ENABLED,
|
||||
CM1106_ABC_DISABLED,
|
||||
};
|
||||
|
||||
class CM1106Component final : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -21,26 +13,13 @@ class CM1106Component final : public PollingComponent, public uart::UARTDevice {
|
||||
void dump_config() override;
|
||||
|
||||
void calibrate_zero(uint16_t ppm);
|
||||
void abc_enable() { this->abc_set_(true); }
|
||||
void abc_disable() { this->abc_set_(false); }
|
||||
|
||||
void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; }
|
||||
void set_abc(bool enabled, uint8_t cycle_days, uint16_t baseline_ppm) {
|
||||
this->abc_boot_logic_ = enabled ? CM1106_ABC_ENABLED : CM1106_ABC_DISABLED;
|
||||
this->abc_cycle_ = cycle_days;
|
||||
this->abc_baseline_ = baseline_ppm;
|
||||
}
|
||||
|
||||
protected:
|
||||
bool cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, size_t response_len);
|
||||
/// Sends a command and checks the sensor's four byte acknowledgement, updating the warning status.
|
||||
bool send_command_expect_ack_(const uint8_t *command, size_t command_len, const uint8_t *ack);
|
||||
void abc_set_(bool enabled);
|
||||
|
||||
sensor::Sensor *co2_sensor_{nullptr};
|
||||
CM1106ABCLogic abc_boot_logic_{CM1106_ABC_NONE};
|
||||
uint8_t abc_cycle_{15};
|
||||
uint16_t abc_baseline_{400};
|
||||
|
||||
bool cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, size_t response_len);
|
||||
};
|
||||
|
||||
} // namespace esphome::cm1106
|
||||
|
||||
@@ -4,12 +4,9 @@ from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, uart
|
||||
from esphome.components.const import CONF_AUTOMATIC_BASELINE_CALIBRATION
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BASELINE,
|
||||
CONF_CO2,
|
||||
CONF_CYCLE,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
ICON_MOLECULE_CO2,
|
||||
@@ -21,22 +18,11 @@ from esphome.types import ConfigType
|
||||
DEPENDENCIES = ["uart"]
|
||||
CODEOWNERS = ["@andrewjswan"]
|
||||
|
||||
|
||||
# true enables ABC with these defaults, false disables it, a mapping enables it with custom values
|
||||
_ABC_SETTINGS_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_CYCLE, default=15): cv.int_range(min=1, max=90),
|
||||
cv.Optional(CONF_BASELINE, default=400): cv.int_range(min=400, max=10000),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
cm1106_ns = cg.esphome_ns.namespace("cm1106")
|
||||
CM1106Component = cm1106_ns.class_(
|
||||
"CM1106Component", cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -48,9 +34,6 @@ CONFIG_SCHEMA = (
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.Any(
|
||||
cv.boolean, _ABC_SETTINGS_SCHEMA
|
||||
),
|
||||
},
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
@@ -74,11 +57,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_CO2, var.set_co2_sensor)
|
||||
|
||||
if (abc := config.get(CONF_AUTOMATIC_BASELINE_CALIBRATION)) is not None:
|
||||
enabled = abc if isinstance(abc, bool) else True
|
||||
settings = _ABC_SETTINGS_SCHEMA({}) if isinstance(abc, bool) else abc
|
||||
cg.add(var.set_abc(enabled, settings[CONF_CYCLE], settings[CONF_BASELINE]))
|
||||
|
||||
|
||||
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
@@ -92,10 +70,3 @@ automation.register_apply_action(
|
||||
CALIBRATION_ACTION_SCHEMA,
|
||||
automation.ApplyCall("calibrate_zero(400)"),
|
||||
)
|
||||
for _name, _call in (
|
||||
("cm1106.abc_enable", "abc_enable()"),
|
||||
("cm1106.abc_disable", "abc_disable()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, CALIBRATION_ACTION_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
|
||||
@@ -8,17 +8,11 @@ BYTE_ORDER_BIG = "big_endian"
|
||||
|
||||
CONF_ACCELEROMETER_ODR = "accelerometer_odr"
|
||||
CONF_ACCELEROMETER_RANGE = "accelerometer_range"
|
||||
CONF_ADC_AVERAGING = "adc_averaging"
|
||||
CONF_ADC_TIME = "adc_time"
|
||||
CONF_ALLOWED_IPS = "allowed_ips"
|
||||
CONF_AQI = "aqi"
|
||||
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
|
||||
CONF_B_CONSTANT = "b_constant"
|
||||
CONF_BLUE_CHANNEL = "blue_channel"
|
||||
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CHANNEL_COLORS = "channel_colors"
|
||||
CONF_CLEAR_CHANNEL = "clear_channel"
|
||||
CONF_CLIMATE_ID = "climate_id"
|
||||
CONF_CO2_EQUIVALENT = "co2_equivalent"
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
@@ -31,7 +25,6 @@ CONF_DESCRIPTION = "description"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_GREEN_CHANNEL = "green_channel"
|
||||
CONF_GYROSCOPE_ODR = "gyroscope_odr"
|
||||
CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
||||
CONF_HOLD_STATE = "hold_state"
|
||||
@@ -50,23 +43,19 @@ CONF_ON_PACKET = "on_packet"
|
||||
CONF_ON_RECEIVE = "on_receive"
|
||||
CONF_ON_SCAN_END = "on_scan_end"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_ON_STOP = "on_stop"
|
||||
CONF_PARITY = "parity"
|
||||
CONF_RECEIVER_FREQUENCY = "receiver_frequency"
|
||||
CONF_RECONNECT_INTERVAL = "reconnect_interval"
|
||||
CONF_RED_CHANNEL = "red_channel"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
CONF_ROLE = "role"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_SCAN_PARAMETERS = "scan_parameters"
|
||||
CONF_SERIAL_NUMBER = "serial_number"
|
||||
CONF_SHA256 = "sha256"
|
||||
CONF_SLOT = "slot"
|
||||
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
|
||||
CONF_STOP_BITS = "stop_bits"
|
||||
CONF_TARGET_COUNT = "target_count"
|
||||
CONF_USE_PSRAM = "use_psram"
|
||||
CONF_VERIFY_SSL = "verify_ssl"
|
||||
CONF_VOC_INDEX = "voc_index"
|
||||
CONF_VOLUME_INCREMENT = "volume_increment"
|
||||
CONF_VOLUME_INITIAL = "volume_initial"
|
||||
@@ -82,4 +71,3 @@ KEY_METADATA = "metadata"
|
||||
|
||||
UNIT_AMPERE_HOUR = "Ah"
|
||||
UNIT_COUNTS = "#"
|
||||
UNIT_MILLIAMPERE_HOUR = "mAh"
|
||||
|
||||
@@ -18,40 +18,21 @@ static constexpr int CSE7761_UREF = 42563; // RmsUc
|
||||
static constexpr int CSE7761_IREF = 52241; // RmsIAC
|
||||
static constexpr int CSE7761_PREF = 44513; // PowerPAC
|
||||
|
||||
// Empirical Sonoff POW CT factor taken from Tasmota
|
||||
static constexpr uint32_t CURRENT_TRANSFORMER_COEFFICIENT_SCALE = 5;
|
||||
|
||||
// PGA 16x on both current channels
|
||||
static constexpr uint16_t SYSCON_SHUNT = 0xFF04;
|
||||
// All PGAs 1x, ADC2ON
|
||||
static constexpr uint16_t SYSCON_CURRENT_TRANSFORMER = 0xFE00;
|
||||
|
||||
// System clock (3.579545MHz) / 8, divided by Ufreq gives the line frequency in Hz
|
||||
static constexpr float CSE7761_UFREQ_CLOCK = 3579545.0f / 8.0f;
|
||||
|
||||
static constexpr uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04)
|
||||
static constexpr uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000)
|
||||
static constexpr uint8_t CSE7761_REG_HFCONST = 0x02; // (2) Pulse frequency register (0x1000)
|
||||
static constexpr uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001)
|
||||
static constexpr uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210)
|
||||
|
||||
static constexpr uint8_t CSE7761_REG_UFREQ = 0x23; // (2) Voltage frequency (0x0000)
|
||||
static constexpr uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000)
|
||||
static constexpr uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000)
|
||||
static constexpr uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000)
|
||||
static constexpr uint8_t CSE7761_REG_POWERFACTOR = 0x27; // (3) Power factor of the selected channel (0x7FFFFF)
|
||||
static constexpr uint8_t CSE7761_REG_ENERGY_PA = 0x28; // (3) Channel A active energy pulse count (0x000000)
|
||||
static constexpr uint8_t CSE7761_REG_ENERGY_PB = 0x29; // (3) Channel B active energy pulse count (0x000000)
|
||||
static constexpr uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000)
|
||||
static constexpr uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000)
|
||||
static constexpr uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register
|
||||
static constexpr uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000)
|
||||
static constexpr uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000)
|
||||
static constexpr uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000)
|
||||
static constexpr uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000)
|
||||
static constexpr uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000)
|
||||
static constexpr uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register
|
||||
|
||||
static constexpr uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum
|
||||
static constexpr uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient
|
||||
|
||||
// Datasheet: Wh = pulses * EnergyXC * HFConst * 1000 / 2^41 with K1 = K2 = 1
|
||||
static constexpr float CSE7761_ENERGY_WH_SCALE = 1000.0f / 2199023255552.0f;
|
||||
|
||||
static constexpr uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command
|
||||
static constexpr uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets
|
||||
static constexpr uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation
|
||||
@@ -76,18 +57,7 @@ void CSE7761Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Current input: %s",
|
||||
LOG_STR_ARG(this->is_current_transformer_() ? LOG_STR("current transformer") : LOG_STR("shunt")));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current 1", this->current_sensor_1_);
|
||||
LOG_SENSOR(" ", "Current 2", this->current_sensor_2_);
|
||||
LOG_SENSOR(" ", "Power 1", this->power_sensor_1_);
|
||||
LOG_SENSOR(" ", "Power 2", this->power_sensor_2_);
|
||||
LOG_SENSOR(" ", "Frequency", this->frequency_sensor_);
|
||||
LOG_SENSOR(" ", "Power Factor 1", this->power_factor_sensor_1_);
|
||||
LOG_SENSOR(" ", "Energy 1", this->energy_sensors_[0]);
|
||||
LOG_SENSOR(" ", "Energy 2", this->energy_sensors_[1]);
|
||||
}
|
||||
|
||||
void CSE7761Component::update() {
|
||||
@@ -103,7 +73,7 @@ void CSE7761Component::write_(uint8_t reg, uint16_t data) {
|
||||
buffer[1] = reg;
|
||||
uint32_t len = 2;
|
||||
if (data) {
|
||||
if (data <= 0xFF) {
|
||||
if (data < 0xFF) {
|
||||
buffer[2] = data & 0xFF;
|
||||
len = 3;
|
||||
} else {
|
||||
@@ -111,7 +81,11 @@ void CSE7761Component::write_(uint8_t reg, uint16_t data) {
|
||||
buffer[3] = data & 0xFF;
|
||||
len = 4;
|
||||
}
|
||||
buffer[len] = ~sum_bytes(buffer, len);
|
||||
uint8_t crc = 0;
|
||||
for (uint32_t i = 0; i < len; i++) {
|
||||
crc += buffer[i];
|
||||
}
|
||||
buffer[len] = ~crc;
|
||||
len++;
|
||||
}
|
||||
|
||||
@@ -141,44 +115,46 @@ bool CSE7761Component::read_once_(uint8_t reg, uint8_t size, uint32_t *value) {
|
||||
}
|
||||
|
||||
rcvd--;
|
||||
if (static_cast<uint8_t>(~sum_bytes(buffer, rcvd, 0xA5 + reg)) != buffer[rcvd]) {
|
||||
return false;
|
||||
}
|
||||
uint32_t result = 0;
|
||||
// CRC check
|
||||
uint8_t crc = 0xA5 + reg;
|
||||
for (uint32_t i = 0; i < rcvd; i++) {
|
||||
result = (result << 8) | buffer[i];
|
||||
crc += buffer[i];
|
||||
}
|
||||
crc = ~crc;
|
||||
if (crc != buffer[rcvd]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*value = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CSE7761Component::read_(uint8_t reg, uint8_t size, uint32_t *value) {
|
||||
for (uint8_t retry = 0; retry < 3; retry++) {
|
||||
if (this->read_once_(reg, size, value))
|
||||
return true;
|
||||
uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) {
|
||||
bool result = false; // Start loop
|
||||
uint8_t retry = 3; // Retry up to three times
|
||||
uint32_t value = 0; // Default no value
|
||||
while (!result && retry > 0) {
|
||||
retry--;
|
||||
if (this->read_once_(reg, size, &value))
|
||||
return value;
|
||||
}
|
||||
ESP_LOGE(TAG, "Reading register %hhu failed!", reg);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) {
|
||||
uint32_t value = 0;
|
||||
return this->read_(reg, size, &value) ? value : 0;
|
||||
return value;
|
||||
}
|
||||
|
||||
uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) {
|
||||
const uint32_t scale = this->is_current_transformer_() ? CURRENT_TRANSFORMER_COEFFICIENT_SCALE : 1;
|
||||
uint32_t coeff = 0;
|
||||
switch (unit) {
|
||||
case RMS_UC:
|
||||
coeff = this->data_.coefficient[RMS_UC];
|
||||
return coeff ? 0x400000 * 100 / coeff : 0;
|
||||
case RMS_IAC:
|
||||
coeff = this->data_.coefficient[RMS_IAC] * scale;
|
||||
coeff = this->data_.coefficient[RMS_IAC];
|
||||
return coeff ? (0x800000 * 100 / coeff) * 10 : 0; // Stay within 32 bits
|
||||
case POWER_PAC:
|
||||
coeff = this->data_.coefficient[POWER_PAC] * scale;
|
||||
coeff = this->data_.coefficient[POWER_PAC];
|
||||
return coeff ? 0x80000000 / coeff : 0;
|
||||
}
|
||||
return 0;
|
||||
@@ -191,38 +167,21 @@ bool CSE7761Component::chip_init_() {
|
||||
calc_chksum += this->data_.coefficient[i];
|
||||
}
|
||||
calc_chksum = ~calc_chksum;
|
||||
const bool energy_configured = this->energy_sensors_[0] != nullptr || this->energy_sensors_[1] != nullptr;
|
||||
uint16_t coeff_chksum = this->read_(CSE7761_REG_COEFFCHKSUM, 2);
|
||||
if ((calc_chksum != coeff_chksum) || (!calc_chksum)) {
|
||||
ESP_LOGD(TAG, "Default calibration");
|
||||
this->data_.coefficient[RMS_IAC] = CSE7761_IREF;
|
||||
this->data_.coefficient[RMS_UC] = CSE7761_UREF;
|
||||
this->data_.coefficient[POWER_PAC] = CSE7761_PREF;
|
||||
if (energy_configured) {
|
||||
ESP_LOGW(TAG, "No energy calibration, energy unavailable");
|
||||
}
|
||||
} else if (energy_configured) {
|
||||
uint32_t hf_const = 0;
|
||||
if (!this->read_(CSE7761_REG_HFCONST, 2, &hf_const)) {
|
||||
ESP_LOGW(TAG, "HFConst read failed, energy unavailable");
|
||||
}
|
||||
// The pulse counters follow the raw power, so they need the same front end factor as power
|
||||
const float input_scale = this->is_current_transformer_() ? CURRENT_TRANSFORMER_COEFFICIENT_SCALE : 1.0f;
|
||||
for (uint8_t channel = 0; channel < 2; channel++) {
|
||||
uint32_t scale = this->data_.coefficient[ENERGY_AC + channel] * hf_const;
|
||||
this->energy_[channel].wh_per_pulse = static_cast<float>(scale) * CSE7761_ENERGY_WH_SCALE * input_scale;
|
||||
}
|
||||
}
|
||||
|
||||
this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_ENABLE_WRITE);
|
||||
|
||||
uint8_t sys_status = this->read_(CSE7761_REG_SYSSTATUS, 1);
|
||||
if (sys_status & 0x10) { // Write enable to protected registers (WREN)
|
||||
this->write_(CSE7761_REG_SYSCON | 0x80,
|
||||
this->is_current_transformer_() ? SYSCON_CURRENT_TRANSFORMER : SYSCON_SHUNT);
|
||||
this->write_(CSE7761_REG_SYSCON | 0x80, 0xFF04);
|
||||
this->write_(CSE7761_REG_EMUCON | 0x80, 0x1183);
|
||||
// WaveEN|ZxEN, needed for Ufreq
|
||||
this->write_(CSE7761_REG_EMUCON2 | 0x80, this->frequency_sensor_ != nullptr ? 0x0FE5 : 0x0FC1);
|
||||
this->write_(CSE7761_REG_EMUCON2 | 0x80, 0x0FC1);
|
||||
this->write_(CSE7761_REG_PULSE1SEL | 0x80, 0x3290);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Write failed at chip_init");
|
||||
@@ -240,15 +199,14 @@ void CSE7761Component::get_data_() {
|
||||
uint32_t value = this->read_(CSE7761_REG_RMSU, 3);
|
||||
this->data_.voltage_rms = (value >= 0x800000) ? 0 : value;
|
||||
|
||||
// No load threshold of 10mA with a shunt; the raw value stands for 5 times more with a current transformer
|
||||
value = this->read_(CSE7761_REG_RMSIA, 3);
|
||||
this->data_.current_rms[0] = ((value >= 0x800000) || (value < 1600)) ? 0 : value;
|
||||
this->data_.current_rms[0] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA
|
||||
value = this->read_(CSE7761_REG_POWERPA, 4);
|
||||
// PowerPA is two's complement signed 32-bit per datasheet
|
||||
this->data_.active_power[0] = (0 == this->data_.current_rms[0]) ? 0 : static_cast<int32_t>(value);
|
||||
|
||||
value = this->read_(CSE7761_REG_RMSIB, 3);
|
||||
this->data_.current_rms[1] = ((value >= 0x800000) || (value < 1600)) ? 0 : value;
|
||||
this->data_.current_rms[1] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA
|
||||
value = this->read_(CSE7761_REG_POWERPB, 4);
|
||||
// PowerPB is two's complement signed 32-bit per datasheet
|
||||
this->data_.active_power[1] = (0 == this->data_.current_rms[1]) ? 0 : static_cast<int32_t>(value);
|
||||
@@ -260,35 +218,12 @@ void CSE7761Component::get_data_() {
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
}
|
||||
|
||||
uint32_t raw_reg = 0;
|
||||
if (this->frequency_sensor_ != nullptr && this->read_(CSE7761_REG_UFREQ, 2, &raw_reg)) {
|
||||
// Ufreq highest bit set means no signal
|
||||
float frequency = (raw_reg == 0 || raw_reg >= 0x8000) ? 0.0f : CSE7761_UFREQ_CLOCK / raw_reg;
|
||||
this->frequency_sensor_->publish_state(frequency);
|
||||
}
|
||||
|
||||
if (this->power_factor_sensor_1_ != nullptr && this->read_(CSE7761_REG_POWERFACTOR, 3, &raw_reg)) {
|
||||
// ADC2ON makes the chip report channel A
|
||||
int24_t pf{static_cast<int32_t>(raw_reg)};
|
||||
this->power_factor_sensor_1_->publish_state(static_cast<float>(static_cast<int32_t>(pf)) / 0x800000);
|
||||
}
|
||||
|
||||
for (uint8_t channel = 0; channel < 2; channel++) {
|
||||
// Active power = PowerPA * PowerPAC * 1000 / 0x80000000
|
||||
float active_power =
|
||||
static_cast<float>(this->data_.active_power[channel]) / this->coefficient_by_unit_(POWER_PAC); // W
|
||||
float amps = static_cast<float>(this->data_.current_rms[channel]) / this->coefficient_by_unit_(RMS_IAC); // A
|
||||
ESP_LOGD(TAG, "Channel %d power %f W, current %f A", channel + 1, active_power, amps);
|
||||
sensor::Sensor *energy_sensor = this->energy_sensors_[channel];
|
||||
CSE7761EnergyChannel &energy = this->energy_[channel];
|
||||
uint8_t energy_reg = (channel == 0) ? CSE7761_REG_ENERGY_PA : CSE7761_REG_ENERGY_PB;
|
||||
uint32_t raw = 0;
|
||||
if (energy_sensor != nullptr && energy.wh_per_pulse > 0.0f && this->read_(energy_reg, 3, &raw)) {
|
||||
// 24-bit counter, not cleared on read
|
||||
energy.pulses += (raw - energy.last_pulses) & 0xFFFFFF;
|
||||
energy.last_pulses = raw;
|
||||
energy_sensor->publish_state(energy.pulses * energy.wh_per_pulse);
|
||||
}
|
||||
if (channel == 0) {
|
||||
if (this->power_sensor_1_ != nullptr) {
|
||||
this->power_sensor_1_->publish_state(active_power);
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/datatypes.h"
|
||||
|
||||
namespace esphome::cse7761 {
|
||||
|
||||
struct CSE7761DataStruct {
|
||||
uint32_t frequency = 0;
|
||||
uint32_t voltage_rms = 0;
|
||||
uint32_t current_rms[2] = {0};
|
||||
int32_t active_power[2] = {0};
|
||||
@@ -15,32 +15,14 @@ struct CSE7761DataStruct {
|
||||
bool ready = false;
|
||||
};
|
||||
|
||||
struct CSE7761EnergyChannel {
|
||||
uint32_t last_pulses{0};
|
||||
uint32_t pulses{0};
|
||||
float wh_per_pulse{0.0f};
|
||||
};
|
||||
|
||||
enum class CSE7761CurrentInput : uint8_t {
|
||||
CSE7761_CURRENT_INPUT_SHUNT,
|
||||
CSE7761_CURRENT_INPUT_CURRENT_TRANSFORMER,
|
||||
};
|
||||
|
||||
/// This class implements support for the CSE7761 UART power sensor.
|
||||
class CSE7761Component final : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
explicit CSE7761Component(CSE7761CurrentInput current_input) : current_input_(current_input) {}
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
|
||||
void set_power_factor_1_sensor(sensor::Sensor *power_factor_sensor_1) {
|
||||
power_factor_sensor_1_ = power_factor_sensor_1;
|
||||
}
|
||||
void set_active_power_1_sensor(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; }
|
||||
void set_current_1_sensor(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; }
|
||||
void set_energy_1_sensor(sensor::Sensor *energy_sensor_1) { this->energy_sensors_[0] = energy_sensor_1; }
|
||||
void set_active_power_2_sensor(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; }
|
||||
void set_current_2_sensor(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; }
|
||||
void set_energy_2_sensor(sensor::Sensor *energy_sensor_2) { this->energy_sensors_[1] = energy_sensor_2; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
@@ -48,23 +30,14 @@ class CSE7761Component final : public PollingComponent, public uart::UARTDevice
|
||||
protected:
|
||||
// Sensors
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *frequency_sensor_{nullptr};
|
||||
sensor::Sensor *power_factor_sensor_1_{nullptr};
|
||||
sensor::Sensor *power_sensor_1_{nullptr};
|
||||
sensor::Sensor *current_sensor_1_{nullptr};
|
||||
sensor::Sensor *power_sensor_2_{nullptr};
|
||||
sensor::Sensor *current_sensor_2_{nullptr};
|
||||
sensor::Sensor *energy_sensors_[2]{nullptr, nullptr};
|
||||
CSE7761DataStruct data_;
|
||||
CSE7761EnergyChannel energy_[2];
|
||||
const CSE7761CurrentInput current_input_;
|
||||
|
||||
bool is_current_transformer_() const {
|
||||
return this->current_input_ == CSE7761CurrentInput::CSE7761_CURRENT_INPUT_CURRENT_TRANSFORMER;
|
||||
}
|
||||
void write_(uint8_t reg, uint16_t data);
|
||||
bool read_once_(uint8_t reg, uint8_t size, uint32_t *value);
|
||||
bool read_(uint8_t reg, uint8_t size, uint32_t *value);
|
||||
uint32_t read_(uint8_t reg, uint8_t size);
|
||||
uint32_t coefficient_by_unit_(uint32_t unit);
|
||||
bool chip_init_();
|
||||
|
||||
@@ -2,22 +2,15 @@ import esphome.codegen as cg
|
||||
from esphome.components import sensor, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FREQUENCY,
|
||||
CONF_ID,
|
||||
CONF_VOLTAGE,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_FREQUENCY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_HERTZ,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -33,41 +26,17 @@ CONF_CURRENT_1 = "current_1"
|
||||
CONF_CURRENT_2 = "current_2"
|
||||
CONF_ACTIVE_POWER_1 = "active_power_1"
|
||||
CONF_ACTIVE_POWER_2 = "active_power_2"
|
||||
CONF_ENERGY_1 = "energy_1"
|
||||
CONF_ENERGY_2 = "energy_2"
|
||||
CONF_POWER_FACTOR_1 = "power_factor_1"
|
||||
CONF_CURRENT_INPUT = "current_input"
|
||||
|
||||
CSE7761CurrentInput = cse7761_ns.enum("CSE7761CurrentInput", is_class=True)
|
||||
CURRENT_INPUTS = {
|
||||
"shunt": CSE7761CurrentInput.CSE7761_CURRENT_INPUT_SHUNT,
|
||||
"current_transformer": CSE7761CurrentInput.CSE7761_CURRENT_INPUT_CURRENT_TRANSFORMER,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CSE7761Component),
|
||||
cv.Optional(CONF_CURRENT_INPUT, default="shunt"): cv.enum(
|
||||
CURRENT_INPUTS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HERTZ,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_FREQUENCY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER_FACTOR_1): sensor.sensor_schema(
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER_FACTOR,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT_1): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
@@ -92,18 +61,6 @@ CONFIG_SCHEMA = (
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ENERGY_1): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional(CONF_ENERGY_2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
@@ -122,17 +79,19 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_CURRENT_INPUT])
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
await sensors(CONF_FREQUENCY, var.set_frequency_sensor)
|
||||
await sensors(CONF_POWER_FACTOR_1, var.set_power_factor_1_sensor)
|
||||
await sensors(CONF_CURRENT_1, var.set_current_1_sensor)
|
||||
await sensors(CONF_CURRENT_2, var.set_current_2_sensor)
|
||||
await sensors(CONF_ACTIVE_POWER_1, var.set_active_power_1_sensor)
|
||||
await sensors(CONF_ACTIVE_POWER_2, var.set_active_power_2_sensor)
|
||||
await sensors(CONF_ENERGY_1, var.set_energy_1_sensor)
|
||||
await sensors(CONF_ENERGY_2, var.set_energy_2_sensor)
|
||||
for key in [
|
||||
CONF_VOLTAGE,
|
||||
CONF_CURRENT_1,
|
||||
CONF_CURRENT_2,
|
||||
CONF_ACTIVE_POWER_1,
|
||||
CONF_ACTIVE_POWER_2,
|
||||
]:
|
||||
if key not in config:
|
||||
continue
|
||||
conf = config[key]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(getattr(var, f"set_{key}_sensor")(sens))
|
||||
|
||||
@@ -67,7 +67,10 @@ bool CSE7766Component::check_byte_() {
|
||||
}
|
||||
|
||||
if (index == CSE7766_RAW_DATA_SIZE - 1) {
|
||||
const uint8_t checksum = sum_bytes(this->raw_data_ + 2, CSE7766_RAW_DATA_SIZE - 3);
|
||||
uint8_t checksum = 0;
|
||||
for (uint8_t i = 2; i < CSE7766_RAW_DATA_SIZE - 1; i++) {
|
||||
checksum += this->raw_data_[i];
|
||||
}
|
||||
|
||||
if (checksum != this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]) {
|
||||
ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum,
|
||||
@@ -202,10 +205,9 @@ void CSE7766Component::parse_data_() {
|
||||
float pf = NAN;
|
||||
if (apparent_power > 0) {
|
||||
pf = power / apparent_power;
|
||||
if (pf > 1) {
|
||||
// Noise near the chip's resolution limit, seen during inrush, pushes the ratio past unity
|
||||
ESP_LOGD(TAG, "Power factor %.4f above 1, clamped", pf);
|
||||
pf = 1.0f;
|
||||
if (pf < 0 || pf > 1) {
|
||||
ESP_LOGD(TAG, "Impossible power factor: %.4f not in interval [0, 1]", pf);
|
||||
pf = NAN;
|
||||
}
|
||||
} else if (apparent_power == 0 && power == 0) {
|
||||
// No load, report ideal power factor
|
||||
|
||||
@@ -1,40 +1,25 @@
|
||||
#include "daikin.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::daikin {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "daikin.climate");
|
||||
|
||||
static constexpr uint32_t BOOST_TIMEOUT_ID = 0;
|
||||
|
||||
void DaikinClimate::setup() {
|
||||
climate_ir::ClimateIR::setup();
|
||||
// A boost preset restored from flash gets its timer too
|
||||
this->update_boost_timer_(false);
|
||||
}
|
||||
|
||||
void DaikinClimate::control(const climate::ClimateCall &call) {
|
||||
const bool was_boost = this->preset == climate::CLIMATE_PRESET_BOOST;
|
||||
climate_ir::ClimateIR::control(call);
|
||||
this->update_boost_timer_(was_boost);
|
||||
}
|
||||
|
||||
void DaikinClimate::transmit_state() {
|
||||
uint8_t remote_state[35] = {0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0x00, 0x11, 0xDA, 0x27, 0x00,
|
||||
uint8_t remote_state[35] = {0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0xD7, 0x11, 0xDA, 0x27, 0x00,
|
||||
0x42, 0x49, 0x05, 0xA2, 0x11, 0xDA, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00};
|
||||
|
||||
this->set_presets_(remote_state);
|
||||
remote_state[21] = this->operation_mode_();
|
||||
remote_state[22] = this->temperature_();
|
||||
uint16_t fan_speed = this->fan_speed_();
|
||||
remote_state[24] = fan_speed >> 8;
|
||||
remote_state[25] = fan_speed & 0xff;
|
||||
|
||||
remote_state[7] = sum_bytes(remote_state, 7);
|
||||
remote_state[34] = sum_bytes(remote_state + 16, 18);
|
||||
// Calculate checksum
|
||||
for (int i = 16; i < 34; i++) {
|
||||
remote_state[34] += remote_state[i];
|
||||
}
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
@@ -151,52 +136,18 @@ uint8_t DaikinClimate::temperature_() const {
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
return 0xc0;
|
||||
default:
|
||||
return static_cast<uint8_t>(
|
||||
roundf(clamp<float>(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX) * 2.0f));
|
||||
uint8_t temperature = (uint8_t) roundf(clamp<float>(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX));
|
||||
return temperature << 1;
|
||||
}
|
||||
}
|
||||
|
||||
void DaikinClimate::set_presets_(uint8_t *remote_state) const {
|
||||
switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) {
|
||||
case climate::CLIMATE_PRESET_BOOST:
|
||||
remote_state[29] = DAIKIN_PRESET_POWERFUL_ON;
|
||||
break;
|
||||
case climate::CLIMATE_PRESET_SLEEP:
|
||||
remote_state[29] = DAIKIN_PRESET_QUIET_ON;
|
||||
break;
|
||||
case climate::CLIMATE_PRESET_ECO:
|
||||
remote_state[32] = DAIKIN_PRESET_ECONO_ON;
|
||||
break;
|
||||
case climate::CLIMATE_PRESET_ACTIVITY:
|
||||
remote_state[32] = DAIKIN_PRESET_SENSOR_ON;
|
||||
break;
|
||||
case climate::CLIMATE_PRESET_COMFORT:
|
||||
remote_state[6] = DAIKIN_PRESET_COMFORT_ON; // the only preset carried by the first frame
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DaikinClimate::update_boost_timer_(bool was_boost) {
|
||||
const bool is_boost = this->preset == climate::CLIMATE_PRESET_BOOST;
|
||||
if (is_boost && !was_boost) {
|
||||
this->set_timeout(BOOST_TIMEOUT_ID, DAIKIN_BOOST_DURATION_MS, [this]() { this->cancel_boost_mode_(); });
|
||||
} else if (was_boost && !is_boost) {
|
||||
this->cancel_timeout(BOOST_TIMEOUT_ID);
|
||||
}
|
||||
}
|
||||
|
||||
void DaikinClimate::cancel_boost_mode_() {
|
||||
if (this->preset != climate::CLIMATE_PRESET_BOOST)
|
||||
return;
|
||||
ESP_LOGD(TAG, "Boost ended");
|
||||
this->preset = climate::CLIMATE_PRESET_NONE;
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
|
||||
const bool was_boost = this->preset == climate::CLIMATE_PRESET_BOOST;
|
||||
uint8_t checksum = 0;
|
||||
for (int i = 0; i < (DAIKIN_STATE_FRAME_SIZE - 1); i++) {
|
||||
checksum += frame[i];
|
||||
}
|
||||
if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum)
|
||||
return false;
|
||||
uint8_t mode = frame[5];
|
||||
// Temperature is given in degrees celcius * 2
|
||||
// only update for states that use the temperature
|
||||
@@ -255,42 +206,16 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_QUIET;
|
||||
break;
|
||||
}
|
||||
|
||||
// Comfort lives in the first frame, recorded as pending until this frame proves the transmission valid
|
||||
// A comfort frame whose own state frame never came must not attach to a later transmission
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const PendingComfort comfort = now - this->pending_comfort_at_ <= DAIKIN_TRANSMISSION_MS
|
||||
? this->pending_comfort_
|
||||
: PendingComfort::PENDING_COMFORT_UNKNOWN;
|
||||
this->pending_comfort_ = PendingComfort::PENDING_COMFORT_UNKNOWN;
|
||||
if (frame[13] & DAIKIN_PRESET_POWERFUL_ON) {
|
||||
this->preset = climate::CLIMATE_PRESET_BOOST;
|
||||
} else if (frame[13] & DAIKIN_PRESET_QUIET_ON) {
|
||||
this->preset = climate::CLIMATE_PRESET_SLEEP;
|
||||
} else if (frame[16] & DAIKIN_PRESET_ECONO_ON) {
|
||||
this->preset = climate::CLIMATE_PRESET_ECO;
|
||||
} else if (frame[16] & DAIKIN_PRESET_SENSOR_ON) {
|
||||
this->preset = climate::CLIMATE_PRESET_ACTIVITY;
|
||||
} else if (comfort == PendingComfort::PENDING_COMFORT_ON) {
|
||||
this->preset = climate::CLIMATE_PRESET_COMFORT;
|
||||
} else if (comfort == PendingComfort::PENDING_COMFORT_OFF || this->preset != climate::CLIMATE_PRESET_COMFORT) {
|
||||
// Without a comfort frame in this transmission a current comfort preset is left alone
|
||||
this->preset = climate::CLIMATE_PRESET_NONE;
|
||||
}
|
||||
this->update_boost_timer_(was_boost);
|
||||
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
uint8_t frame[DAIKIN_STATE_FRAME_SIZE] = {};
|
||||
uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE] = {};
|
||||
if (!data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t frame_size = DAIKIN_STATE_FRAME_SIZE;
|
||||
for (uint8_t pos = 0; pos < frame_size; pos++) {
|
||||
for (uint8_t pos = 0; pos < DAIKIN_STATE_FRAME_SIZE; pos++) {
|
||||
uint8_t byte = 0;
|
||||
for (int8_t bit = 0; bit < 8; bit++) {
|
||||
if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
|
||||
@@ -299,7 +224,7 @@ bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
frame[pos] = byte;
|
||||
state_frame[pos] = byte;
|
||||
if (pos == 0) {
|
||||
// frame header
|
||||
if (byte != 0x11)
|
||||
@@ -318,25 +243,11 @@ bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
return false;
|
||||
} else if (pos == 4) {
|
||||
// frame type
|
||||
if (byte == DAIKIN_FRAME_TYPE_COMFORT) {
|
||||
frame_size = DAIKIN_COMFORT_FRAME_SIZE;
|
||||
} else if (byte != DAIKIN_FRAME_TYPE_STATE) {
|
||||
if (byte != 0x00)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (frame[frame_size - 1] != sum_bytes(frame, frame_size - 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frame_size == DAIKIN_COMFORT_FRAME_SIZE) {
|
||||
// Nothing changes yet: the state frame that follows applies it once it has passed its own checksum
|
||||
this->pending_comfort_ = (frame[6] & DAIKIN_PRESET_COMFORT_ON) ? PendingComfort::PENDING_COMFORT_ON
|
||||
: PendingComfort::PENDING_COMFORT_OFF;
|
||||
this->pending_comfort_at_ = App.get_loop_component_start_time();
|
||||
return true;
|
||||
}
|
||||
return this->parse_state_frame_(frame);
|
||||
return this->parse_state_frame_(state_frame);
|
||||
}
|
||||
|
||||
} // namespace esphome::daikin
|
||||
|
||||
@@ -27,17 +27,6 @@ const uint8_t DAIKIN_FAN_3 = 0x50;
|
||||
const uint8_t DAIKIN_FAN_4 = 0x60;
|
||||
const uint8_t DAIKIN_FAN_5 = 0x70;
|
||||
|
||||
// Presets: bit flags in byte 6 (comfort), byte 29 (powerful, quiet) and byte 32 (sensor, econo)
|
||||
const uint8_t DAIKIN_PRESET_COMFORT_ON = 0x10;
|
||||
const uint8_t DAIKIN_PRESET_POWERFUL_ON = 0x01;
|
||||
const uint8_t DAIKIN_PRESET_QUIET_ON = 0x20;
|
||||
const uint8_t DAIKIN_PRESET_ECONO_ON = 0x04;
|
||||
const uint8_t DAIKIN_PRESET_SENSOR_ON = 0x02;
|
||||
// The unit leaves powerful mode by itself after 20 minutes; mirror that in the entity
|
||||
const uint32_t DAIKIN_BOOST_DURATION_MS = 20 * 60 * 1000;
|
||||
// The three frames of one transmission arrive well inside this window
|
||||
const uint32_t DAIKIN_TRANSMISSION_MS = 1000;
|
||||
|
||||
// IR Transmission
|
||||
const uint32_t DAIKIN_IR_FREQUENCY = 38000;
|
||||
const uint32_t DAIKIN_HEADER_MARK = 3360;
|
||||
@@ -47,44 +36,27 @@ const uint32_t DAIKIN_ONE_SPACE = 1370;
|
||||
const uint32_t DAIKIN_ZERO_SPACE = 360;
|
||||
const uint32_t DAIKIN_MESSAGE_SPACE = 32300;
|
||||
|
||||
// Frame types (byte 4) and sizes
|
||||
const uint8_t DAIKIN_FRAME_TYPE_STATE = 0x00;
|
||||
const uint8_t DAIKIN_FRAME_TYPE_COMFORT = 0xC5;
|
||||
// State Frame size
|
||||
const uint8_t DAIKIN_STATE_FRAME_SIZE = 19;
|
||||
const uint8_t DAIKIN_COMFORT_FRAME_SIZE = 8;
|
||||
|
||||
class DaikinClimate final : public climate_ir::ClimateIR {
|
||||
public:
|
||||
DaikinClimate()
|
||||
: climate_ir::ClimateIR(
|
||||
DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 0.5f, true, true,
|
||||
{climate::CLIMATE_FAN_QUIET, climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW,
|
||||
climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_BOTH},
|
||||
{climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_COMFORT,
|
||||
climate::CLIMATE_PRESET_SLEEP, climate::CLIMATE_PRESET_ACTIVITY, climate::CLIMATE_PRESET_NONE}) {}
|
||||
: climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_QUIET, climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW,
|
||||
climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
|
||||
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||
|
||||
protected:
|
||||
void setup() override;
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
uint8_t operation_mode_() const;
|
||||
uint16_t fan_speed_() const;
|
||||
uint8_t temperature_() const;
|
||||
void set_presets_(uint8_t *remote_state) const;
|
||||
/// Arms or cancels the boost timer when the preset moves into or out of boost.
|
||||
void update_boost_timer_(bool was_boost);
|
||||
void cancel_boost_mode_();
|
||||
// Handle received IR Buffer
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
bool parse_state_frame_(const uint8_t frame[]);
|
||||
|
||||
/// What the 0xC5 frame of the current transmission said about comfort, applied by the state frame
|
||||
enum class PendingComfort : uint8_t { PENDING_COMFORT_UNKNOWN, PENDING_COMFORT_OFF, PENDING_COMFORT_ON };
|
||||
PendingComfort pending_comfort_{PendingComfort::PENDING_COMFORT_UNKNOWN};
|
||||
uint32_t pending_comfort_at_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::daikin
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user