mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Compare commits
86
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4cfbffe92 | ||
|
|
a58074c343 | ||
|
|
7f2c3ba3f5 | ||
|
|
a97604d574 | ||
|
|
1146b77836 | ||
|
|
1338902ac5 | ||
|
|
60a1d70805 | ||
|
|
887d4c3a83 | ||
|
|
c1fa73470e | ||
|
|
e41d16ba7e | ||
|
|
5171113c87 | ||
|
|
5718e4b779 | ||
|
|
40e12cd0d8 | ||
|
|
07029d0077 | ||
|
|
bb1119d83f | ||
|
|
7b591b26d6 | ||
|
|
f45e6f0bf4 | ||
|
|
4b8af08155 | ||
|
|
613973b432 | ||
|
|
9ee4314bc2 | ||
|
|
59f55dbe9e | ||
|
|
8fce95454a | ||
|
|
16134ee993 | ||
|
|
bc39d56bb4 | ||
|
|
c5522abc71 | ||
|
|
7cb5906d2a | ||
|
|
d452dbce30 | ||
|
|
81e5b50087 | ||
|
|
426a4a2dce | ||
|
|
c95c17c4e7 | ||
|
|
576d3325fe | ||
|
|
c9cf0ad92e | ||
|
|
f505212e64 | ||
|
|
52a5e6a007 | ||
|
|
3b261d2cf0 | ||
|
|
9238fcc727 | ||
|
|
96902cc122 | ||
|
|
95711d83d1 | ||
|
|
3cdcf0417a | ||
|
|
c6e90de262 | ||
|
|
92f6ab8367 | ||
|
|
f61955baa2 | ||
|
|
36e1d831d3 | ||
|
|
63616bf642 | ||
|
|
d275926f65 | ||
|
|
f265c46c21 | ||
|
|
7d735f0677 | ||
|
|
5c23a827b6 | ||
|
|
e265c0c0fc | ||
|
|
a0c7d754cb | ||
|
|
a6dbd72c53 | ||
|
|
9d530f5c56 | ||
|
|
6562c49686 | ||
|
|
a97ce3a201 | ||
|
|
5a78f636b9 | ||
|
|
53d504137b | ||
|
|
bb0546fc0b | ||
|
|
b43630cf8e | ||
|
|
c9664d4e42 | ||
|
|
39ea6a3220 | ||
|
|
f4a6d6f18b | ||
|
|
6c24d4aa43 | ||
|
|
0dc1d3b5a1 | ||
|
|
42341d05f9 | ||
|
|
f941fee5ed | ||
|
|
8721976066 | ||
|
|
05bf7d3440 | ||
|
|
918b7d2553 | ||
|
|
417ab4d2e9 | ||
|
|
0821c84b7a | ||
|
|
a827058928 | ||
|
|
3063b76210 | ||
|
|
55261ae7e5 | ||
|
|
1d8423edd8 | ||
|
|
d27ec8ec49 | ||
|
|
5acbd44067 | ||
|
|
40271fb597 | ||
|
|
95766ed3ad | ||
|
|
4a80d785a8 | ||
|
|
c1c1d2b2ae | ||
|
|
29a668af04 | ||
|
|
fd541bca2c | ||
|
|
192ae31e45 | ||
|
|
77c9fa2fa4 | ||
|
|
25843c5242 | ||
|
|
7881dfc5d8 |
@@ -32,7 +32,7 @@ runs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||
# downstream jobs rely on is preserved.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull-request-only workflow: a save could never be shared and
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
|
||||
- name: Upload compile-test image artifact
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
# The tar is already compressed, so upload it as-is. archive: false
|
||||
# skips the redundant zip and makes the file name the artifact name
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Download image artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: compile-test-image.tar.zst
|
||||
- name: Load image
|
||||
|
||||
+11
-11
@@ -49,7 +49,7 @@ jobs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||
# that ``. venv/bin/activate`` see an identical layout.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -438,7 +438,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -477,7 +477,7 @@ jobs:
|
||||
# Tests that compare rendered output write the image they actually got here, so a
|
||||
# failure can be looked at without reproducing the whole build locally.
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: integration-test-artifacts-${{ matrix.bucket.name }}
|
||||
path: test_artifacts/
|
||||
@@ -488,7 +488,7 @@ jobs:
|
||||
# script/update_integration_test_durations.py; only full matrix dev
|
||||
# runs produce usable data.
|
||||
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: junit-integration-${{ strategy.job-index }}
|
||||
path: junit-integration.xml
|
||||
@@ -528,7 +528,7 @@ jobs:
|
||||
script/check_import_time.py --check --har importtime.har
|
||||
- name: Upload waterfall HAR
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: import-time-waterfall
|
||||
path: importtime.har
|
||||
@@ -626,7 +626,7 @@ jobs:
|
||||
apt-get install -y libc6-dbg
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@c4fd08a3a159bd0cc208da1e0edf32b8c47d75e5 # v5.4.0
|
||||
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -1448,7 +1448,7 @@ jobs:
|
||||
# install step (order-of-magnitude faster on cold boots,
|
||||
# with its own wheel cache). actions/setup-python still
|
||||
# provides the interpreter.
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -1647,7 +1647,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -1710,7 +1710,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
@@ -1740,13 +1740,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# setup-python interpreter so subsequent ``prek`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
|
||||
@@ -10,7 +10,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.16.10
|
||||
rev: v0.17.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -36,7 +36,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.21.2
|
||||
rev: v3.22.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py312-plus]
|
||||
|
||||
@@ -144,6 +144,9 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
|
||||
* **C++ Preprocessor Directives:**
|
||||
* **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums.
|
||||
* **A constant with a compile-time initializer is `static constexpr`,** not `static const`, at namespace scope
|
||||
and inside functions; `static const` is only for a value computed at run time. A function-local
|
||||
`static constexpr` table is a static constant to clang-tidy, so it is `UPPER_SNAKE_CASE` too.
|
||||
* **Use `#define` only for:**
|
||||
- Conditional compilation (`#ifdef`, `#ifndef`)
|
||||
- Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`)
|
||||
@@ -217,6 +220,9 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
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`.
|
||||
* **Model-specific code** sits behind a define the Python side emits only for configs that use that model
|
||||
(`cg.add_define("USE_SELEC_METER_EM4M")`), so the other models' builds leave it out; the runtime model check
|
||||
stays 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
|
||||
@@ -567,6 +573,9 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
An intentionally empty body (an unused `register_dumper` builder, for example) gets a one-line docstring, not `pass`.
|
||||
|
||||
* **Configuration Validation:**
|
||||
* **One schema per model:** when a component's options depend on a `model` key, use `cv.typed_schema({...},
|
||||
key=CONF_MODEL, default_type=...)` with each model's own keys and defaults, not one schema plus a validator
|
||||
that rejects keys and fills defaults afterwards.
|
||||
* **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`.
|
||||
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
|
||||
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
|
||||
@@ -609,6 +618,9 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
`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.
|
||||
* **A test that needs a shared component in a particular state** (`esp32_ble` security settings, an option
|
||||
that PSRAM rejects) cannot be grouped: the merged block wins and the whole batch fails. Add the component to
|
||||
`ISOLATED_COMPONENTS` in `script/analyze_component_buses.py`, or move the option to a `test-<variant>` file.
|
||||
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
|
||||
```
|
||||
tests/
|
||||
|
||||
@@ -212,6 +212,7 @@ esphome/components/factory_reset/* @anatoly-savchenkov
|
||||
esphome/components/fastled_base/* @OttoWinter
|
||||
esphome/components/feedback/* @ianchi
|
||||
esphome/components/file/* @esphome/core
|
||||
esphome/components/filter_lifetime/* @tuct
|
||||
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
|
||||
esphome/components/font/* @clydebarrow @esphome/core
|
||||
esphome/components/friedrich/* @rwrozelle
|
||||
@@ -242,6 +243,9 @@ esphome/components/haier/button/* @paveldn
|
||||
esphome/components/haier/sensor/* @paveldn
|
||||
esphome/components/haier/switch/* @paveldn
|
||||
esphome/components/haier/text_sensor/* @paveldn
|
||||
esphome/components/haier_tundra/* @fauxpark
|
||||
esphome/components/haier_tundra/button/* @fauxpark
|
||||
esphome/components/haier_tundra/switch/* @fauxpark
|
||||
esphome/components/havells_solar/* @sourabhjaiswal
|
||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||
esphome/components/hbridge/light/* @DotNetDann
|
||||
@@ -286,6 +290,7 @@ 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/icm20689/* @lboue
|
||||
esphome/components/icnt86/* @danepowell
|
||||
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
||||
esphome/components/improv_base/* @esphome/core
|
||||
@@ -434,6 +439,7 @@ esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/packet_transport/* @clydebarrow
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
esphome/components/pca9505/* @ajfriesen @ssieb
|
||||
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
||||
esphome/components/pcf85063/* @brogon
|
||||
esphome/components/pcf8563/* @KoenBreeman
|
||||
|
||||
+16
-18
@@ -483,12 +483,9 @@ def has_ota() -> bool:
|
||||
|
||||
def has_native_ota() -> bool:
|
||||
"""Check if native API OTA upload is available (``platform: esphome``)."""
|
||||
if CONF_OTA not in CORE.config:
|
||||
return False
|
||||
return any(
|
||||
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
|
||||
for ota_item in CORE.config[CONF_OTA]
|
||||
)
|
||||
from esphome.espota2 import esphome_ota_items
|
||||
|
||||
return bool(esphome_ota_items(CORE.config))
|
||||
|
||||
|
||||
def has_web_server_ota() -> bool:
|
||||
@@ -1359,14 +1356,17 @@ def _choose_ota_platform(config: ConfigType, requested: str | None) -> str:
|
||||
return CONF_WEB_SERVER
|
||||
|
||||
|
||||
def _esphome_ota_conf(config: ConfigType) -> ConfigType | None:
|
||||
"""The validated esphome OTA platform block, if configured."""
|
||||
from esphome.espota2 import esphome_ota_items
|
||||
|
||||
return next(iter(esphome_ota_items(config)), None)
|
||||
|
||||
|
||||
def _upload_via_native_api(
|
||||
config: ConfigType, network_devices: list[str], args: ArgsProtocol
|
||||
) -> tuple[int, str | None]:
|
||||
ota_conf: ConfigType = {}
|
||||
for ota_item in config.get(CONF_OTA, []):
|
||||
if ota_item.get(CONF_PLATFORM) == CONF_ESPHOME:
|
||||
ota_conf = ota_item
|
||||
break
|
||||
ota_conf = _esphome_ota_conf(config) or {}
|
||||
|
||||
from esphome import espota2
|
||||
from esphome.components.noise import static_encryption_key
|
||||
@@ -1457,10 +1457,8 @@ def _old_key_notice(config: ConfigType) -> None:
|
||||
`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):
|
||||
for item in espota2.esphome_ota_items(config):
|
||||
if (item.get(CONF_ENCRYPTION) or {}).get(espota2.CONF_OLD_KEY):
|
||||
_LOGGER.warning(espota2.OLD_KEY_REMOVE_NOTICE)
|
||||
|
||||
|
||||
@@ -1468,12 +1466,12 @@ def _upload_via_web_server(
|
||||
config: ConfigType, network_devices: list[str], binary: Path
|
||||
) -> tuple[int, str | None]:
|
||||
from esphome import web_server_ota
|
||||
from esphome.espota2 import esphome_ota_items
|
||||
from esphome.web_server_helpers import get_web_server_connection
|
||||
|
||||
if any(
|
||||
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
|
||||
and ota_item.get(CONF_ENCRYPTION) is not None
|
||||
for ota_item in config.get(CONF_OTA, [])
|
||||
ota_item.get(CONF_ENCRYPTION) is not None
|
||||
for ota_item in esphome_ota_items(config)
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"This config has OTA encryption, but the web_server OTA path sends "
|
||||
|
||||
+16
-1
@@ -906,6 +906,21 @@ async def while_action_to_code(
|
||||
return var
|
||||
|
||||
|
||||
def _shadow_iteration(args: TemplateArgsType) -> TemplateArgsType:
|
||||
"""Rename an inherited `iteration` so the inner repeat's own counter can shadow it."""
|
||||
names = {name for _, name in args}
|
||||
renamed: TemplateArgsType = []
|
||||
for type_, name in args:
|
||||
if name == "iteration":
|
||||
depth = 1
|
||||
while f"iteration_{depth}" in names:
|
||||
depth += 1
|
||||
name = f"iteration_{depth}"
|
||||
names.add(name)
|
||||
renamed.append((type_, name))
|
||||
return renamed
|
||||
|
||||
|
||||
@register_action(
|
||||
"repeat",
|
||||
RepeatAction,
|
||||
@@ -929,7 +944,7 @@ async def repeat_action_to_code(
|
||||
actions = await build_action_list(
|
||||
config[CONF_THEN],
|
||||
cg.TemplateArguments(cg.uint32, *template_arg.args),
|
||||
[(cg.uint32, "iteration"), *args],
|
||||
[(cg.uint32, "iteration"), *_shadow_iteration(args)],
|
||||
)
|
||||
for action in actions:
|
||||
cg.add(var.add_then_action(action))
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bedjet {
|
||||
|
||||
static const char *const TAG = "bedjet";
|
||||
ESPHOME_LOG_TAG(TAG, "bedjet");
|
||||
|
||||
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
|
||||
inline static uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
const char *const Automation::TAG = "ble_client.automation";
|
||||
const char *const Automation::TAG = "ble_client.automation"; // NOLINT: ble_client never builds for ESP8266
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include "esphome/components/bme280_base/bme280_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bme280_i2c {
|
||||
|
||||
static const char *const TAG = "bme280_i2c.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "bme280_i2c.sensor");
|
||||
|
||||
class BME280I2CComponent final : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
|
||||
bool read_byte(uint8_t a_register, uint8_t *data) override;
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include "esphome/components/bmp280_base/bmp280_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bmp280_i2c {
|
||||
|
||||
static const char *const TAG = "bmp280_i2c.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "bmp280_i2c.sensor");
|
||||
|
||||
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
|
||||
class BMP280I2CComponent final : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include "esphome/components/bmp581_base/bmp581_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bmp581_i2c {
|
||||
|
||||
static const char *const TAG = "bmp581_i2c.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "bmp581_i2c.sensor");
|
||||
|
||||
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
|
||||
class BMP581I2CComponent final : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::chsc6x {
|
||||
|
||||
static const char *const TAG = "chsc6x.touchscreen";
|
||||
ESPHOME_LOG_TAG(TAG, "chsc6x.touchscreen");
|
||||
|
||||
static const uint8_t CHSC6X_REG_STATUS = 0x00;
|
||||
static const uint8_t CHSC6X_REG_STATUS_TOUCH = 0x00;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::cst816 {
|
||||
|
||||
static const char *const TAG = "cst816.touchscreen";
|
||||
ESPHOME_LOG_TAG(TAG, "cst816.touchscreen");
|
||||
|
||||
static const uint8_t REG_STATUS = 0x00;
|
||||
static const uint8_t REG_TOUCH_NUM = 0x02;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::cst9220 {
|
||||
|
||||
static const char *const TAG = "cst9220.touchscreen";
|
||||
ESPHOME_LOG_TAG(TAG, "cst9220.touchscreen");
|
||||
|
||||
// The CST92xx family uses 16-bit (big-endian) register addresses.
|
||||
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
|
||||
|
||||
@@ -143,6 +143,8 @@ def validate_wakeup_pin(
|
||||
processed_pins: list[ConfigType] = [{CONF_PIN: value}]
|
||||
else:
|
||||
processed_pins = list(value)
|
||||
if not processed_pins:
|
||||
raise cv.Invalid("'wakeup_pin' needs at least one entry")
|
||||
|
||||
for i, pin_config in enumerate(processed_pins):
|
||||
# now validate each item
|
||||
@@ -154,29 +156,34 @@ def validate_wakeup_pin(
|
||||
|
||||
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
# right now only BK72XX supports the list format for wakeup pins
|
||||
if CORE.is_bk72xx:
|
||||
if CONF_WAKEUP_PIN_MODE in config:
|
||||
wakeup_pins = config.get(CONF_WAKEUP_PIN, [])
|
||||
if len(wakeup_pins) > 1:
|
||||
raise cv.Invalid(
|
||||
"You need to remove the global wakeup_pin_mode and define it per pin"
|
||||
)
|
||||
if wakeup_pins:
|
||||
if CONF_WAKEUP_PIN_MODE in wakeup_pins[0]:
|
||||
raise cv.Invalid(
|
||||
"Specify wakeup_pin_mode either at the top level under deep_sleep "
|
||||
"or under the pin entry, not both"
|
||||
)
|
||||
wakeup_pins[0][CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE)
|
||||
elif (
|
||||
isinstance(config.get(CONF_WAKEUP_PIN), list)
|
||||
and len(config[CONF_WAKEUP_PIN]) > 1
|
||||
):
|
||||
wakeup_pins = config.get(CONF_WAKEUP_PIN)
|
||||
has_pin_mode = CONF_WAKEUP_PIN_MODE in config
|
||||
if has_pin_mode and wakeup_pins is None:
|
||||
raise cv.Invalid(
|
||||
"Your platform does not support providing multiple entries in wakeup_pin"
|
||||
"'wakeup_pin_mode' requires 'wakeup_pin'", path=[CONF_WAKEUP_PIN_MODE]
|
||||
)
|
||||
|
||||
multiple_pins = wakeup_pins is not None and len(wakeup_pins) > 1
|
||||
# wakeup_pin is limited to ESP32 and BK72XX, and only BK72XX accepts a list
|
||||
if not CORE.is_bk72xx:
|
||||
if multiple_pins:
|
||||
raise cv.Invalid(
|
||||
"ESP32 does not support multiple entries for 'wakeup_pin'",
|
||||
path=[CONF_WAKEUP_PIN],
|
||||
)
|
||||
elif has_pin_mode:
|
||||
if multiple_pins:
|
||||
raise cv.Invalid(
|
||||
"Remove the global 'wakeup_pin_mode' and define it per pin",
|
||||
path=[CONF_WAKEUP_PIN_MODE],
|
||||
)
|
||||
if CONF_WAKEUP_PIN_MODE in (pin := wakeup_pins[0]):
|
||||
raise cv.Invalid(
|
||||
"Specify 'wakeup_pin_mode' either at the top level under 'deep_sleep' "
|
||||
"or under the pin entry, not both",
|
||||
path=[CONF_WAKEUP_PIN, 0, CONF_WAKEUP_PIN_MODE],
|
||||
)
|
||||
# Move the global mode into the single pin entry, which to_code() reads
|
||||
pin[CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE)
|
||||
return config
|
||||
|
||||
|
||||
@@ -203,11 +210,11 @@ def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
|
||||
if CORE.is_bk72xx:
|
||||
max_duration = core.TimePeriod(hours=36)
|
||||
if value > max_duration:
|
||||
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
|
||||
raise cv.Invalid("'sleep_duration' cannot be more than 36 hours on BK72XX")
|
||||
elif CORE.using_zephyr:
|
||||
max_duration = core.TimePeriod(days=49)
|
||||
if value > max_duration:
|
||||
raise cv.Invalid("sleep duration cannot be more than 49 days on Zephyr")
|
||||
raise cv.Invalid("'sleep_duration' cannot be more than 49 days on Zephyr")
|
||||
return value
|
||||
|
||||
|
||||
@@ -290,7 +297,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
_validate_sleep_duration,
|
||||
),
|
||||
cv.Optional(CONF_WAKEUP_PIN): validate_wakeup_pin,
|
||||
cv.Optional(CONF_WAKEUP_PIN): cv.All(
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]),
|
||||
validate_wakeup_pin,
|
||||
),
|
||||
cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All(
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]),
|
||||
cv.enum(WAKEUP_PIN_MODES, upper=True),
|
||||
@@ -347,11 +357,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if CONF_SLEEP_DURATION in config:
|
||||
cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION]))
|
||||
if CONF_WAKEUP_PIN in config:
|
||||
pins_as_list = config.get(CONF_WAKEUP_PIN, [])
|
||||
if (wakeup_pins := config.get(CONF_WAKEUP_PIN)) is not None:
|
||||
if CORE.is_bk72xx:
|
||||
cg.add(var.init_wakeup_pins(len(pins_as_list)))
|
||||
for item in pins_as_list:
|
||||
cg.add(var.init_wakeup_pins(len(wakeup_pins)))
|
||||
for item in wakeup_pins:
|
||||
cg.add(
|
||||
var.add_wakeup_pin(
|
||||
await cg.gpio_pin_expression(item[CONF_PIN]),
|
||||
@@ -361,7 +370,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
)
|
||||
else:
|
||||
pin = await cg.gpio_pin_expression(pins_as_list[0][CONF_PIN])
|
||||
pin = await cg.gpio_pin_expression(wakeup_pins[0][CONF_PIN])
|
||||
cg.add(var.set_wakeup_pin(pin))
|
||||
if CONF_WAKEUP_PIN_MODE in config:
|
||||
cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE]))
|
||||
|
||||
@@ -250,6 +250,42 @@ def add_metadata(
|
||||
)
|
||||
|
||||
|
||||
KEY_EXTERNAL_WRITERS = "external_writers"
|
||||
|
||||
|
||||
def _get_external_writer_list() -> list[tuple[ID, str]]:
|
||||
"""Get the raw external writer list. Each entry is (display id, owner)."""
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_EXTERNAL_WRITERS, [])
|
||||
|
||||
|
||||
def register_external_writer(display_id: ID, owner: str) -> None:
|
||||
"""Declare that ``owner`` draws into the display from outside its lambda or pages.
|
||||
|
||||
Call from the owner's CONFIG_SCHEMA. The ID may still be unresolved; the ID pass
|
||||
resolves it in place before final validation.
|
||||
"""
|
||||
_get_external_writer_list().append((display_id, owner))
|
||||
|
||||
|
||||
def get_external_writers(display_id: ID) -> list[str]:
|
||||
"""Get the owners registered for a display. Only valid once IDs are resolved."""
|
||||
return [
|
||||
owner
|
||||
for id_, owner in _get_external_writer_list()
|
||||
if id_.id is not None and id_.id == display_id.id
|
||||
]
|
||||
|
||||
|
||||
def has_external_writer(display_id: ID) -> bool:
|
||||
"""Tell a driver's final validation that something else draws into the display.
|
||||
|
||||
LVGL anywhere in the config also counts, as the drivers assumed before.
|
||||
"""
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
return LVGL_DOMAIN in full_config.get() or bool(get_external_writers(display_id))
|
||||
|
||||
|
||||
async def register_display(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await setup_display_core_(var, config)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::display {
|
||||
@@ -52,40 +53,65 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
|
||||
this->line(x1, y1, x2, y2, color);
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
uint32_t color_value;
|
||||
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
|
||||
// -Os stops inlining it once there are two call sites, which costs a call and
|
||||
// spills per pixel. Without PSRAM there is one call site and it changes
|
||||
// nothing, and draw_pixel_at() stays a virtual call either way.
|
||||
template<ColorBitness BITNESS>
|
||||
static void __attribute__((flatten))
|
||||
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
// BITNESS is a constant here, so to_color()'s three per pixel divisions
|
||||
// become a multiply and shift and its bitness switch folds.
|
||||
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
|
||||
display->draw_pixel_at(
|
||||
x + x_start, y + y_start,
|
||||
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
|
||||
};
|
||||
// The source and frame buffers are row-major, so walking rows keeps the read
|
||||
// and the write sequential. When the display swaps the axes before writing,
|
||||
// walking columns keeps the write sequential instead; with the frame buffer
|
||||
// in PSRAM a strided write costs more than a strided read.
|
||||
#ifdef USE_PSRAM
|
||||
if (display->pixel_axes_swapped()) {
|
||||
size_t column_idx = y_offset * line_stride + x_offset;
|
||||
for (int x = 0; x != w; x++, column_idx++) {
|
||||
size_t source_idx = column_idx;
|
||||
for (int y = 0; y != h; y++, source_idx += line_stride) {
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
|
||||
for (int y = 0; y != h; y++) {
|
||||
size_t source_idx = (y_offset + y) * line_stride + x_offset;
|
||||
size_t source_idx_mod;
|
||||
for (int x = 0; x != w; x++, source_idx++) {
|
||||
switch (bitness) {
|
||||
default:
|
||||
color_value = ptr[source_idx];
|
||||
break;
|
||||
case COLOR_BITNESS_565:
|
||||
source_idx_mod = source_idx * 2;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
|
||||
}
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
source_idx_mod = source_idx * 3;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
switch (bitness) {
|
||||
case COLOR_BITNESS_565:
|
||||
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
default:
|
||||
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = x; i < x + width; i++)
|
||||
|
||||
@@ -139,6 +139,11 @@ enum DisplayRotation {
|
||||
DISPLAY_ROTATION_270_DEGREES = 270,
|
||||
};
|
||||
|
||||
/// Whether a rotation swaps the x and y axes.
|
||||
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
|
||||
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
|
||||
}
|
||||
|
||||
const int EDGES_TRIGON = 3;
|
||||
const int EDGES_TRIANGLE = 3;
|
||||
const int EDGES_TETRAGON = 4;
|
||||
@@ -717,6 +722,12 @@ class Display : public PollingComponent {
|
||||
|
||||
virtual DisplayRotation get_rotation() const { return this->rotation_; }
|
||||
|
||||
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
|
||||
/// neighbours along x land a row apart in the frame buffer. Not the same as
|
||||
/// get_width()/get_height() swapping: a display that rotates in hardware
|
||||
/// reports swapped dimensions but writes unswapped pixels.
|
||||
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
|
||||
|
||||
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
|
||||
* returns the type the display is currently configured to.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::display {
|
||||
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
|
||||
@@ -8,6 +11,27 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
|
||||
|
||||
class ColorUtil {
|
||||
public:
|
||||
/// Reads one packed pixel of the given bitness from a source buffer into the
|
||||
/// value to_color() takes. BITNESS is a template argument so the byte layout
|
||||
/// folds; pass it on to to_color() as a constant too, and its channel scaling
|
||||
/// divides by a constant, which the compiler turns into a multiply and shift.
|
||||
template<ColorBitness BITNESS>
|
||||
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
|
||||
if constexpr (BITNESS == COLOR_BITNESS_565) {
|
||||
const size_t idx = index * 2;
|
||||
if (big_endian)
|
||||
return (ptr[idx] << 8) + ptr[idx + 1];
|
||||
return ptr[idx] + (ptr[idx + 1] << 8);
|
||||
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
|
||||
const size_t idx = index * 3;
|
||||
if (big_endian)
|
||||
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
|
||||
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
|
||||
} else {
|
||||
return ptr[index];
|
||||
}
|
||||
}
|
||||
|
||||
static Color to_color(uint32_t colorcode, ColorOrder color_order,
|
||||
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
|
||||
uint8_t first_color, second_color, third_color;
|
||||
@@ -154,4 +178,54 @@ class ColorUtil {
|
||||
return color;
|
||||
}
|
||||
};
|
||||
|
||||
/** Rec.601 luma weights (0.299/0.587/0.114), scaled by 256 */
|
||||
static constexpr uint16_t REC601_LUMA_R = 77;
|
||||
static constexpr uint16_t REC601_LUMA_G = 150;
|
||||
static constexpr uint16_t REC601_LUMA_B = 29;
|
||||
|
||||
/** Rec.601 luma of an RGB color, for optimum perceptual brightness */
|
||||
constexpr uint8_t rec601_luma(Color color) {
|
||||
return (uint8_t) ((REC601_LUMA_R * color.r + REC601_LUMA_G * color.g + REC601_LUMA_B * color.b + 128u) >> 8);
|
||||
}
|
||||
|
||||
/** Map an RGB color to a single monochrome bit: 1 = white, 0 = black */
|
||||
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
|
||||
|
||||
/** color_to_mono() of an RGB565 value as ColorUtil::to_color() reads it, from table lookups instead of a division
|
||||
* per channel.
|
||||
*/
|
||||
class Rgb565ToMono {
|
||||
public:
|
||||
explicit Rgb565ToMono(ColorOrder order) {
|
||||
// Rec.601 weight of the 5-bit, 6-bit and 5-bit fields, by the channel each one holds in this order
|
||||
uint16_t high = REC601_LUMA_R, middle = REC601_LUMA_G, low = REC601_LUMA_B;
|
||||
if (order == COLOR_ORDER_BGR) {
|
||||
high = REC601_LUMA_B;
|
||||
low = REC601_LUMA_R;
|
||||
} else if (order == COLOR_ORDER_GRB) {
|
||||
high = REC601_LUMA_G;
|
||||
middle = REC601_LUMA_R;
|
||||
}
|
||||
// Each field value scaled to 0..255, then weighted
|
||||
for (unsigned i = 0; i != 32; i++) {
|
||||
this->high_[i] = high * esp_scale(i, 31);
|
||||
this->low_[i] = low * esp_scale(i, 31);
|
||||
}
|
||||
for (unsigned i = 0; i != 64; i++)
|
||||
this->middle_[i] = middle * esp_scale(i, 63);
|
||||
}
|
||||
|
||||
uint8_t operator()(uint16_t value) const {
|
||||
const uint32_t weighted = this->high_[value >> 11] + this->middle_[(value >> 5) & 0x3F] + this->low_[value & 0x1F];
|
||||
return ((weighted + 128u) >> 8) >= 128 ? 1 : 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
// Weighted contribution of each value of each field
|
||||
std::array<uint16_t, 32> high_{};
|
||||
std::array<uint16_t, 64> middle_{};
|
||||
std::array<uint16_t, 32> low_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::display
|
||||
|
||||
@@ -23,11 +23,18 @@ DEPENDENCIES = ["uart"]
|
||||
|
||||
emontx_ns = cg.esphome_ns.namespace("emontx")
|
||||
EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice)
|
||||
LockOnReboot = emontx_ns.enum("LockOnReboot", is_class=True)
|
||||
|
||||
CONF_EMONTX_ID = "emontx_id"
|
||||
CONF_TAG_NAME = "tag_name"
|
||||
CONF_ON_JSON = "on_json"
|
||||
CONF_LOCK_ON_REBOOT = "lock_on_reboot"
|
||||
LOCK_ON_REBOOT_AUTO = "auto"
|
||||
LOCK_ON_REBOOT_MODES = {
|
||||
LOCK_ON_REBOOT_AUTO: LockOnReboot.LOCK_ON_REBOOT_AUTO,
|
||||
True: LockOnReboot.LOCK_ON_REBOOT_ALWAYS,
|
||||
False: LockOnReboot.LOCK_ON_REBOOT_NEVER,
|
||||
}
|
||||
|
||||
DOMAIN = "emontx"
|
||||
|
||||
@@ -59,7 +66,9 @@ CONFIG_SCHEMA = (
|
||||
cv.GenerateID(): cv.declare_id(EmonTx),
|
||||
cv.Optional(CONF_ON_JSON): _synchronous_automation_schema(),
|
||||
cv.Optional(CONF_ON_DATA): _synchronous_automation_schema(),
|
||||
cv.Optional(CONF_LOCK_ON_REBOOT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_LOCK_ON_REBOOT, default=LOCK_ON_REBOOT_AUTO): cv.Any(
|
||||
cv.boolean, cv.one_of(LOCK_ON_REBOOT_AUTO, lower=True)
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -135,8 +144,11 @@ async def to_code(config: ConfigType) -> None:
|
||||
await uart.register_uart_device(var, config)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if config[CONF_LOCK_ON_REBOOT]:
|
||||
cg.add(var.set_lock_on_reboot(True))
|
||||
lock = config[CONF_LOCK_ON_REBOOT]
|
||||
cg.add(var.set_lock_on_reboot(LOCK_ON_REBOOT_MODES[lock]))
|
||||
if lock == LOCK_ON_REBOOT_AUTO:
|
||||
# Locking depends on the firmware version the emonTx reports
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
|
||||
|
||||
# Action: emontx.send_command
|
||||
|
||||
@@ -1,23 +1,91 @@
|
||||
#include "emontx.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
|
||||
namespace esphome::emontx {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "emontx");
|
||||
|
||||
static constexpr uint32_t UNLOCK_ID = 0;
|
||||
// The emonTx joins bytes received while a command is pending into that command, so the next one waits
|
||||
static constexpr uint32_t COMMAND_GAP_MS = 250;
|
||||
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
static constexpr uint32_t VERSION_REQUEST_ID = 1;
|
||||
// Without a reply, 'v' is sent again every VERSION_RETRY_FRAMES data lines, VERSION_MAX_ATTEMPTS times in all
|
||||
static constexpr uint8_t VERSION_RETRY_FRAMES = 2;
|
||||
static constexpr uint8_t VERSION_MAX_ATTEMPTS = 3;
|
||||
// Line of the 'v' reply (and of the boot banner) carrying the release, e.g. " - Version: v1.1.1"
|
||||
static const char VERSION_PREFIX[] PROGMEM = " - Version:";
|
||||
// Every data line starts with the message counter, in JSON or text format
|
||||
static const char MSG_JSON_PREFIX[] PROGMEM = "{\"MSG\":";
|
||||
static const char MSG_TEXT_PREFIX[] PROGMEM = "MSG:";
|
||||
// First firmware with 'emonlock' and 'emonunlock'; older firmware takes them for the bootloader command
|
||||
static constexpr uint32_t LOCK_MIN_MAJOR = 1;
|
||||
static constexpr uint32_t LOCK_MIN_MINOR = 2;
|
||||
|
||||
// True if line starts with the PROGMEM prefix and has more after it
|
||||
static bool starts_with(const char *line, size_t len, const char *prefix, size_t prefix_len) {
|
||||
return len > prefix_len && ESPHOME_strncmp_P(line, prefix, prefix_len) == 0;
|
||||
}
|
||||
|
||||
// Reads the decimal number (up to 9 digits) at pos and moves pos past it; false if there is none
|
||||
static bool read_number(const char *&pos, const char *end, uint32_t &value) {
|
||||
const char *start = pos;
|
||||
value = 0;
|
||||
while (pos < end && pos - start < 9 && *pos >= '0' && *pos <= '9')
|
||||
value = value * 10 + (*pos++ - '0');
|
||||
return pos != start;
|
||||
}
|
||||
|
||||
// True for a release such as "v1.2.0" or "v1.3.0-rc1" from LOCK_MIN_MAJOR.LOCK_MIN_MINOR on
|
||||
static bool supports_lock(const char *version, const char *end) {
|
||||
if (version < end && (*version == 'v' || *version == 'V'))
|
||||
version++;
|
||||
uint32_t major;
|
||||
if (!read_number(version, end, major) || version == end || *version != '.')
|
||||
return false;
|
||||
version++;
|
||||
uint32_t minor;
|
||||
if (!read_number(version, end, minor))
|
||||
return false;
|
||||
return major > LOCK_MIN_MAJOR || (major == LOCK_MIN_MAJOR && minor >= LOCK_MIN_MINOR);
|
||||
}
|
||||
#endif
|
||||
|
||||
void EmonTx::setup() {
|
||||
this->buffer_pos_ = 0;
|
||||
if (!this->lock_on_reboot_)
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
|
||||
// The line feed ends any boot noise
|
||||
this->write_byte('\n');
|
||||
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
|
||||
this->unlock_();
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->wants_version_())
|
||||
this->request_version_();
|
||||
#endif
|
||||
});
|
||||
return;
|
||||
// The line feed ends any boot noise first
|
||||
ESP_LOGI(TAG, "Unlocking emonTx after boot");
|
||||
this->write_byte('\n');
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// With AUTO, the reply decides whether to unlock
|
||||
if (this->wants_version_())
|
||||
this->request_version_();
|
||||
#endif
|
||||
}
|
||||
|
||||
void EmonTx::unlock_() {
|
||||
ESP_LOGI(TAG, "Unlocking emonTx");
|
||||
this->send_command("emonunlock");
|
||||
}
|
||||
|
||||
void EmonTx::on_shutdown() {
|
||||
if (!this->lock_on_reboot_)
|
||||
bool lock = this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS;
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
lock |= this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && this->lock_supported_;
|
||||
#endif
|
||||
if (!lock)
|
||||
return;
|
||||
if (this->is_paused()) {
|
||||
ESP_LOGW(TAG, "Not locking emonTx before shutdown, UART is paused");
|
||||
@@ -48,6 +116,11 @@ void EmonTx::loop() {
|
||||
uint8_t dropped;
|
||||
while (this->available() > 0 && this->read_byte(&dropped))
|
||||
this->discarding_line_ = dropped != '\n';
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// The emonTx may have been flashed, or a request dropped, during the pause
|
||||
if (this->wants_version_())
|
||||
this->request_version_();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Read all available data to prevent UART buffer overflow
|
||||
@@ -77,6 +150,12 @@ void EmonTx::loop() {
|
||||
ESP_LOGV(TAG, "Line is JSON, parsing...");
|
||||
this->parse_json_(this->buffer_.data(), len);
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->wants_version_()) {
|
||||
this->parse_msg_number_(this->buffer_.data(), len);
|
||||
this->parse_version_line_(this->buffer_.data(), len);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else if (received == '\r' || this->discarding_line_) {
|
||||
// Ignore CR, and the rest of a line that overflowed or was interrupted by a pause
|
||||
@@ -112,12 +191,91 @@ void EmonTx::parse_json_(const char *data, size_t len) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
void EmonTx::request_version_() {
|
||||
// The firmware may have changed; until the reply, an unknown version gets no lock commands
|
||||
this->lock_supported_ = false;
|
||||
this->version_frames_left_ = VERSION_RETRY_FRAMES * VERSION_MAX_ATTEMPTS;
|
||||
this->send_version_request_();
|
||||
}
|
||||
|
||||
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
|
||||
void EmonTx::send_version_request_() {
|
||||
if (this->is_paused())
|
||||
return;
|
||||
this->write_byte('\n');
|
||||
this->set_timeout(VERSION_REQUEST_ID, COMMAND_GAP_MS, [this]() { this->send_command("v"); });
|
||||
}
|
||||
|
||||
void EmonTx::parse_msg_number_(const char *line, size_t len) {
|
||||
const char *pos;
|
||||
if (starts_with(line, len, MSG_JSON_PREFIX, sizeof(MSG_JSON_PREFIX) - 1)) {
|
||||
pos = line + sizeof(MSG_JSON_PREFIX) - 1;
|
||||
} else if (starts_with(line, len, MSG_TEXT_PREFIX, sizeof(MSG_TEXT_PREFIX) - 1)) {
|
||||
pos = line + sizeof(MSG_TEXT_PREFIX) - 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
uint32_t msg_number;
|
||||
if (!read_number(pos, line + len, msg_number))
|
||||
return;
|
||||
|
||||
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
|
||||
if (msg_number <= this->last_msg_number_) {
|
||||
this->request_version_();
|
||||
} else if (this->version_frames_left_ > 0) {
|
||||
if (--this->version_frames_left_ == 0) {
|
||||
ESP_LOGW(TAG, "No firmware version from the emonTx, it may still be locked");
|
||||
} else if (this->version_frames_left_ % VERSION_RETRY_FRAMES == 0) {
|
||||
this->send_version_request_();
|
||||
}
|
||||
}
|
||||
this->last_msg_number_ = msg_number;
|
||||
}
|
||||
|
||||
void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
constexpr size_t prefix_len = sizeof(VERSION_PREFIX) - 1;
|
||||
if (!starts_with(line, len, VERSION_PREFIX, prefix_len))
|
||||
return;
|
||||
const char *version = line + prefix_len;
|
||||
const char *end = line + len;
|
||||
while (version < end && *version == ' ')
|
||||
version++;
|
||||
if (version == end)
|
||||
return;
|
||||
ESP_LOGI(TAG, "emonTx firmware version: %.*s", static_cast<int>(end - version), version);
|
||||
this->version_frames_left_ = 0;
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
this->version_text_sensor_->publish_state(version, end - version);
|
||||
#endif
|
||||
|
||||
bool supported = supports_lock(version, end);
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && supported && !this->unlock_sent_) {
|
||||
// Once the rest of the reply has gone out; a pause drops it, and the version request on resume retries
|
||||
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
|
||||
if (this->is_paused())
|
||||
return;
|
||||
this->unlock_();
|
||||
this->unlock_sent_ = true;
|
||||
});
|
||||
}
|
||||
this->lock_supported_ = supported;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Logs the EmonTx component configuration details.
|
||||
*/
|
||||
void EmonTx::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "EmonTx:");
|
||||
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", YESNO(this->lock_on_reboot_));
|
||||
const char *lock = LOG_STR_LITERAL("no");
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
|
||||
lock = LOG_STR_LITERAL("yes");
|
||||
} else if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO) {
|
||||
lock = LOG_STR_LITERAL("auto");
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", lock);
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size());
|
||||
@@ -127,6 +285,9 @@ void EmonTx::dump_config() {
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
LOG_TEXT_SENSOR(" ", "Firmware version", this->version_text_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,12 +13,21 @@
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::emontx {
|
||||
|
||||
/// Maximum line length in bytes (plus one byte reserved for null terminator)
|
||||
static constexpr size_t MAX_LINE_LENGTH = 1024;
|
||||
|
||||
enum class LockOnReboot : uint8_t {
|
||||
LOCK_ON_REBOOT_NEVER,
|
||||
LOCK_ON_REBOOT_ALWAYS,
|
||||
LOCK_ON_REBOOT_AUTO, // Only when the emonTx reports firmware v1.2.0 or later
|
||||
};
|
||||
|
||||
/**
|
||||
* @class EmonTx
|
||||
* @brief Main class for the EmonTx component.
|
||||
@@ -27,6 +36,9 @@ static constexpr size_t MAX_LINE_LENGTH = 1024;
|
||||
* extracts tags and values, and publishes them to registered sensors.
|
||||
*/
|
||||
class EmonTx final : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
SUB_TEXT_SENSOR(version)
|
||||
#endif
|
||||
public:
|
||||
EmonTx() = default;
|
||||
|
||||
@@ -50,8 +62,8 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
bool is_paused() const { return this->paused_.load(std::memory_order_relaxed); }
|
||||
|
||||
/// Lock the emonTx ('emonlock') before a reboot, so serial garbage during the reboot is not taken
|
||||
/// as a command, and unlock it in setup(). Needs emonTx firmware v1.2.0 or later.
|
||||
void set_lock_on_reboot(bool lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
|
||||
/// as a command, and unlock it after boot. Needs emonTx firmware v1.2.0 or later, which AUTO checks.
|
||||
void set_lock_on_reboot(LockOnReboot lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void init_sensors(size_t count) { this->sensors_.init(count); }
|
||||
@@ -60,6 +72,20 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
|
||||
protected:
|
||||
void parse_json_(const char *data, size_t len);
|
||||
void unlock_();
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
bool wants_version_() const {
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
return true;
|
||||
#endif
|
||||
return this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO;
|
||||
}
|
||||
void request_version_();
|
||||
void send_version_request_();
|
||||
void parse_msg_number_(const char *line, size_t len);
|
||||
void parse_version_line_(const char *line, size_t len);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
FixedVector<std::pair<const char *, sensor::Sensor *>> sensors_{};
|
||||
@@ -69,8 +95,14 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
uint16_t buffer_pos_{0};
|
||||
std::atomic<bool> paused_{false};
|
||||
bool was_paused_{false};
|
||||
bool lock_on_reboot_{false};
|
||||
LockOnReboot lock_on_reboot_{LockOnReboot::LOCK_ON_REBOOT_NEVER};
|
||||
bool discarding_line_{false}; // Set after an overflow or a pause until the end of that line
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
bool lock_supported_{false}; // The last version reported is v1.2.0 or later
|
||||
bool unlock_sent_{false}; // AUTO has sent 'emonunlock' since boot
|
||||
uint8_t version_frames_left_{0}; // Data lines still to wait for a version reply; 0 when none is owed
|
||||
uint32_t last_msg_number_{0};
|
||||
#endif
|
||||
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_EMONTX_ID, EmonTx
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_EMONTX_ID): cv.use_id(EmonTx),
|
||||
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_EMONTX_ID])
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
if await text_sensors(CONF_VERSION, hub.set_version_text_sensor) is not None:
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
cg.add_define("USE_EMONTX_VERSION_TEXT_SENSOR")
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/color.h"
|
||||
|
||||
/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys
|
||||
@@ -13,21 +14,15 @@
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
// The Rec.601 luma helpers and the RGB565 mono lookup live in the shared display utilities, so other
|
||||
// drivers (e.g. it8951 in its 1 bit per pixel mode) can use them too.
|
||||
using display::color_to_mono;
|
||||
using display::rec601_luma;
|
||||
using display::Rgb565ToMono;
|
||||
|
||||
/** Delta for when to regard as gray */
|
||||
static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50;
|
||||
|
||||
/** Rec.601 luma (0.299/0.587/0.114 weights, scaled by 256) for optimum perceptual brightness */
|
||||
constexpr uint8_t rec601_luma(Color color) {
|
||||
return (uint8_t) ((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
|
||||
}
|
||||
|
||||
/** Map RGB color to a single monochrome bit
|
||||
*
|
||||
* @param color RGB color to convert from
|
||||
* @return 1 = white, 0 = black
|
||||
*/
|
||||
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
|
||||
|
||||
/** Map RGB color to one of 4 discrete gray levels (2 bits per pixel)
|
||||
*
|
||||
* @param color RGB color to convert from
|
||||
|
||||
@@ -40,7 +40,6 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -198,15 +197,11 @@ def _final_validate(config) -> None:
|
||||
"epaper_spi", require_miso=False, require_mosi=True
|
||||
)(config)
|
||||
|
||||
global_config = full_config.get()
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if display.has_external_writer(config[CONF_ID]):
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
else:
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
elif CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
|
||||
|
||||
@@ -131,9 +131,7 @@ void EPaperBase::update() {
|
||||
}
|
||||
|
||||
void EPaperBase::wait_for_idle_(bool should_wait) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
this->waiting_for_idle_start_ = millis();
|
||||
#endif
|
||||
this->waiting_for_idle_ = should_wait;
|
||||
}
|
||||
|
||||
@@ -156,6 +154,10 @@ void EPaperBase::loop() {
|
||||
ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
|
||||
#endif
|
||||
} else {
|
||||
if (now - this->waiting_for_idle_start_ > this->busy_timeout_seconds_ * 1000u) {
|
||||
this->abandon_update_();
|
||||
return;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
if (now - this->waiting_for_idle_last_print_ >= 1000) {
|
||||
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
|
||||
@@ -168,6 +170,20 @@ void EPaperBase::loop() {
|
||||
this->process_state_();
|
||||
}
|
||||
|
||||
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
|
||||
void EPaperBase::abandon_update_() {
|
||||
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
|
||||
this->epaper_state_to_string_());
|
||||
this->status_set_warning(LOG_STR("Busy pin timeout"));
|
||||
if (this->state_ == EPaperState::TRANSFER_DATA)
|
||||
this->abort_transfer();
|
||||
// Leave the panel powered down rather than with its high-voltage drivers enabled
|
||||
this->power_off();
|
||||
this->full_update_requested_ = true;
|
||||
this->current_data_index_ = 0;
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the state machine.
|
||||
* Typical state sequence:
|
||||
@@ -242,6 +258,7 @@ void EPaperBase::process_state_() {
|
||||
break;
|
||||
case EPaperState::DEEP_SLEEP:
|
||||
this->deep_sleep();
|
||||
this->status_clear_warning();
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
|
||||
break;
|
||||
@@ -345,6 +362,120 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// One source row of an RGB565 block, written into a contiguous 1 bit per pixel buffer. Without SwapAxes the row lies
|
||||
// along a buffer row and the bit moves by step; with it the row lies along a bit column and the byte moves by step
|
||||
// rows.
|
||||
template<bool SwapAxes, bool BigEndianSource>
|
||||
void HOT write_rgb565_row(uint8_t *byte, uint8_t mask, int step, size_t row_width, const uint8_t *source, int count,
|
||||
const Rgb565ToMono &to_mono) {
|
||||
const ptrdiff_t byte_step = SwapAxes ? step * static_cast<ptrdiff_t>(row_width) : 0;
|
||||
for (; count != 0; count--, source += 2) {
|
||||
const uint16_t value = BigEndianSource ? (source[0] << 8) | source[1] : source[0] | (source[1] << 8);
|
||||
if (to_mono(value)) {
|
||||
*byte |= mask;
|
||||
} else {
|
||||
*byte &= ~mask;
|
||||
}
|
||||
if constexpr (SwapAxes) {
|
||||
byte += byte_step;
|
||||
} else if (step > 0) {
|
||||
mask >>= 1;
|
||||
if (mask == 0) {
|
||||
mask = 0x80;
|
||||
byte++;
|
||||
}
|
||||
} else {
|
||||
mask <<= 1;
|
||||
if (mask == 0) {
|
||||
mask = 0x01;
|
||||
byte--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Each source column maps to one buffer coordinate and each source row to the other, each moving by 1 per pixel, so
|
||||
// the visible part of a block and its dirty area are worked out once for the whole block.
|
||||
void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr,
|
||||
ColorOrder order, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const bool swap = this->effective_transform_ & SWAP_XY;
|
||||
const bool mirror_x = this->effective_transform_ & MIRROR_X;
|
||||
const bool mirror_y = this->effective_transform_ & MIRROR_Y;
|
||||
// The buffer axis a source column moves along, and the one a source row moves along
|
||||
const int column_limit = swap ? this->height_ : this->width_;
|
||||
const int row_limit = swap ? this->width_ : this->height_;
|
||||
const bool column_mirrored = swap ? mirror_y : mirror_x;
|
||||
const bool row_mirrored = swap ? mirror_x : mirror_y;
|
||||
const int first_column = std::max(0, -x_start);
|
||||
const int end_column = std::min(w, column_limit - x_start);
|
||||
const int first_row = std::max(0, -y_start);
|
||||
const int end_row = std::min(h, row_limit - y_start);
|
||||
if (first_column >= end_column || first_row >= end_row)
|
||||
return;
|
||||
auto column_to_buffer = [&](int column) {
|
||||
const int u = x_start + column;
|
||||
return column_mirrored ? column_limit - 1 - u : u;
|
||||
};
|
||||
auto row_to_buffer = [&](int row) {
|
||||
const int v = y_start + row;
|
||||
return row_mirrored ? row_limit - 1 - v : v;
|
||||
};
|
||||
|
||||
const int column_low = std::min(column_to_buffer(first_column), column_to_buffer(end_column - 1));
|
||||
const int row_low = std::min(row_to_buffer(first_row), row_to_buffer(end_row - 1));
|
||||
const int columns = end_column - first_column;
|
||||
const int rows = end_row - first_row;
|
||||
const int x_low = swap ? row_low : column_low;
|
||||
const int y_low = swap ? column_low : row_low;
|
||||
const int x_high = x_low + (swap ? rows : columns);
|
||||
const int y_high = y_low + (swap ? columns : rows);
|
||||
|
||||
const Rgb565ToMono to_mono(order);
|
||||
const size_t line_stride = x_offset + w + x_pad;
|
||||
const size_t row_width = this->row_width_;
|
||||
const int step = column_mirrored ? -1 : 1;
|
||||
for (int row = first_row; row != end_row; row++) {
|
||||
const uint8_t *source = ptr + ((y_offset + row) * line_stride + x_offset + first_column) * 2;
|
||||
const int along = column_to_buffer(first_column);
|
||||
const int across = row_to_buffer(row);
|
||||
const int x = swap ? across : along;
|
||||
const int y = swap ? along : across;
|
||||
uint8_t *byte = buffer + y * row_width + x / 8;
|
||||
const uint8_t mask = 0x80u >> (x % 8);
|
||||
if (swap) {
|
||||
if (big_endian) {
|
||||
write_rgb565_row<true, true>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
} else {
|
||||
write_rgb565_row<true, false>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
}
|
||||
} else if (big_endian) {
|
||||
write_rgb565_row<false, true>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
} else {
|
||||
write_rgb565_row<false, false>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
}
|
||||
}
|
||||
this->x_low_ = clamp_at_most(this->x_low_, x_low);
|
||||
this->x_high_ = clamp_at_least(this->x_high_, x_high);
|
||||
this->y_low_ = clamp_at_most(this->y_low_, y_low);
|
||||
this->y_high_ = clamp_at_least(this->y_high_, y_high);
|
||||
}
|
||||
|
||||
// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the
|
||||
// usual case): the same result as draw_pixel_at() for each pixel. Anything else goes through draw_pixel_at().
|
||||
void HOT EPaperBase::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
size_t contiguous_length;
|
||||
uint8_t *contiguous = this->buffer_.get_span(0, contiguous_length);
|
||||
if (!this->buffer_is_mono() || this->get_clipping().is_set() || bitness != COLOR_BITNESS_565 ||
|
||||
contiguous_length != this->buffer_length_) {
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
}
|
||||
this->draw_rgb565_block_(contiguous, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset, x_pad);
|
||||
}
|
||||
|
||||
void EPaperBase::dump_config() {
|
||||
LOG_DISPLAY("", "E-Paper SPI", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
|
||||
@@ -106,7 +106,10 @@ class EPaperBase : public Display,
|
||||
|
||||
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
// Make the next update a full one. Applied when that update starts, so one in progress is not affected.
|
||||
void request_full_update() { this->full_update_requested_ = true; }
|
||||
// True from the start of an update until the display has refreshed and gone back to sleep.
|
||||
@@ -125,9 +128,14 @@ class EPaperBase : public Display,
|
||||
virtual bool initialise(bool partial);
|
||||
void send_init_sequence_(const uint8_t *sequence, size_t length);
|
||||
void wait_for_idle_(bool should_wait);
|
||||
void abandon_update_();
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
void update_effective_transform_();
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
void draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad);
|
||||
// Whether the buffer holds 1 bit per pixel as draw_pixel_at() writes it, so draw_pixels_at() can write it directly
|
||||
virtual bool buffer_is_mono() const { return false; }
|
||||
|
||||
/**
|
||||
* Methods that must be implemented by concrete classes to control the display
|
||||
@@ -137,6 +145,8 @@ class EPaperBase : public Display,
|
||||
* @return true if done, false if it should be called next loop
|
||||
*/
|
||||
virtual bool transfer_data() = 0;
|
||||
// Called when an update is abandoned in the middle of transfer_data(), to release anything it left open
|
||||
virtual void abort_transfer() {}
|
||||
/**
|
||||
* Refresh the screen after data transfer
|
||||
*/
|
||||
@@ -187,18 +197,20 @@ class EPaperBase : public Display,
|
||||
// point to the pixel past the last one updated, i.e. may range up to width/height.
|
||||
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
|
||||
|
||||
uint32_t waiting_for_idle_start_{};
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
uint32_t waiting_for_idle_last_print_{};
|
||||
uint32_t waiting_for_idle_start_{};
|
||||
#endif
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
|
||||
uint32_t update_start_time_{};
|
||||
#endif
|
||||
|
||||
// properties with specific initialisers go last
|
||||
EPaperState state_{EPaperState::IDLE};
|
||||
uint32_t reset_duration_{10};
|
||||
// Longer than any panel takes to refresh; past it the busy pin is assumed stuck
|
||||
uint16_t busy_timeout_seconds_{120};
|
||||
uint8_t full_update_every_{1};
|
||||
EPaperState state_{EPaperState::IDLE};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -15,6 +15,7 @@ class EPaperMono : public EPaperBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return true; }
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override {}
|
||||
void power_off() override{};
|
||||
|
||||
@@ -58,7 +58,9 @@ bool HOT EPaperSSD1677::transfer_data() {
|
||||
// A full update ignores 0x26, and the copy may not hold a real frame yet (first update after
|
||||
// boot): send the new frame to both planes.
|
||||
const bool send_copy = this->plane_ == 0 && this->update_count_ != 0;
|
||||
const bool direct = send_copy || this->buffer_is_plane();
|
||||
// A mono buffer already holds the frame as a RAM plane does, so it can be sent as it is; a deeper
|
||||
// buffer (the four-level model) returns false and packs the plane in plane_row().
|
||||
const bool direct = send_copy || this->buffer_is_mono();
|
||||
const auto &source = send_copy ? this->sent_ : this->buffer_;
|
||||
const size_t row_length = this->plane_row_length_();
|
||||
const size_t plane_length = row_length * this->height_;
|
||||
|
||||
@@ -34,9 +34,6 @@ class EPaperSSD1677 : public EPaperMono {
|
||||
void init_comparison_frame_();
|
||||
// Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds.
|
||||
size_t plane_row_length_() const { return (this->width_ + 7) / 8; }
|
||||
// Whether the buffer already holds the frame as a RAM plane does, so it can be sent as it is.
|
||||
// A subclass with a deeper buffer returns false and overrides plane_row().
|
||||
virtual bool buffer_is_plane() const { return true; }
|
||||
// Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white.
|
||||
virtual void plane_row(size_t y, uint8_t *out);
|
||||
bool reset() override;
|
||||
|
||||
@@ -38,9 +38,9 @@ class EPaperSSD1677Gray4 : public EPaperSSD1677 {
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return false; }
|
||||
// A partial update if partial updates are enabled and this is not a full one.
|
||||
bool is_partial_push_() const { return this->update_count_ != 0 && this->sent_.is_valid(); }
|
||||
bool buffer_is_plane() const override { return false; }
|
||||
void plane_row(size_t y, uint8_t *out) override;
|
||||
void refresh_screen(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
|
||||
@@ -364,6 +364,15 @@ bool HOT EPaperT133A01::transfer_data() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperT133A01::abort_transfer() {
|
||||
// A data stream is open, with one of the chip selects held low, whenever transfer_data() has started sending rows
|
||||
if (this->current_data_index_ == 0)
|
||||
return;
|
||||
this->disable();
|
||||
this->cs_pin_->digital_write(true);
|
||||
this->cs1_pin_->digital_write(true);
|
||||
}
|
||||
|
||||
void EPaperT133A01::dump_config() {
|
||||
EPaperBase::dump_config();
|
||||
LOG_PIN(" CS Pin: ", this->cs_pin_);
|
||||
|
||||
@@ -46,6 +46,7 @@ class EPaperT133A01 : public EPaperBase {
|
||||
void deep_sleep() override;
|
||||
|
||||
bool transfer_data() override;
|
||||
void abort_transfer() override;
|
||||
|
||||
/**
|
||||
* Send a command (and optional data) selecting one or both controllers.
|
||||
|
||||
@@ -37,6 +37,7 @@ class EPaperUC8179 final : public EPaperBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return true; }
|
||||
bool initialise(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
|
||||
@@ -87,6 +87,38 @@ jd79660.extend(
|
||||
),
|
||||
)
|
||||
|
||||
# Waveshare 2.13-G (V2 hardware/init revision, PCB marked "rev 2.1" or later)
|
||||
#
|
||||
# Vendor init derived from vendor sample code
|
||||
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g_V2.cpp>
|
||||
# Compatible MIT license, see esphome/LICENSE file.
|
||||
#
|
||||
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
|
||||
# Note: panel is 122px wide, but vendor code declares the TRES register as a fixed
|
||||
# 124 on this revision. width=124 is declared here (rather than 122) purely so the
|
||||
# transferred bytes-per-row matches what the vendor init tells the IC to expect; the
|
||||
# extra 2 columns fall outside the visible glass. The model width must equal the
|
||||
# TRES value in initsequence, so overriding dimensions: in YAML brings the shear back.
|
||||
#
|
||||
# fmt: off
|
||||
jd79660.extend(
|
||||
"Waveshare-2.13in-G",
|
||||
width=124,
|
||||
height=250,
|
||||
|
||||
initsequence=(
|
||||
(0x61, 0x00, 0x7C, 0x00, 0xFA,), # TRES: 124x250 fixed (124 = TRES stride value)
|
||||
(0xE9, 0x01,),
|
||||
# Power On (0x04): Must be early part of init seq = Disabled later!
|
||||
(0x04,),
|
||||
),
|
||||
fast_update=(
|
||||
(0xE0, 0x02,),
|
||||
(0xE6, 0x5A,),
|
||||
(0xA5,),
|
||||
),
|
||||
)
|
||||
|
||||
# Waveshare 7.5-H
|
||||
#
|
||||
# Vendor init derived from vendor sample code
|
||||
@@ -130,7 +162,7 @@ jd79660.extend(
|
||||
# Note: busy pin uses LOW=busy, HIGH=idle.
|
||||
#
|
||||
# fmt: off
|
||||
jd79660.extend(
|
||||
gdey0213f51 = jd79660.extend(
|
||||
"goodisplay-gdey0213f51-2.13",
|
||||
width=128,
|
||||
height=250,
|
||||
@@ -154,3 +186,13 @@ jd79660.extend(
|
||||
(0x04,),
|
||||
),
|
||||
)
|
||||
|
||||
# Waveshare 2.13-G (V1 hardware/init revision, older stock): the same panel and init as the Good
|
||||
# Display entry above, byte for byte, so it only adds the name. Vendor sample:
|
||||
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g.cpp>
|
||||
# Compatible MIT license, see esphome/LICENSE file.
|
||||
#
|
||||
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
|
||||
# Note: width=128 is the TRES value the init sequence programs, not the 122 visible columns; overriding
|
||||
# dimensions: in YAML shears the image. Not tested on V1 hardware.
|
||||
gdey0213f51.extend("Waveshare-2.13in-G-V1")
|
||||
|
||||
@@ -24,3 +24,11 @@ WaveshareB(
|
||||
data_rate="10MHz",
|
||||
minimum_update_interval="1s",
|
||||
)
|
||||
|
||||
WaveshareB(
|
||||
"waveshare-2.66in-b",
|
||||
width=152,
|
||||
height=296,
|
||||
data_rate="10MHz",
|
||||
minimum_update_interval="15s",
|
||||
)
|
||||
|
||||
@@ -111,24 +111,36 @@ void ES8388::setup() {
|
||||
// Power on ADC, Enable LIN&RIN, Power off MICBIAS, set int1lp to low power mode
|
||||
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCPOWER, 0x09));
|
||||
|
||||
#ifdef USE_SELECT
|
||||
#ifdef USE_ES8388_SELECT
|
||||
if (this->dac_output_select_ != nullptr) {
|
||||
auto dac_power = this->get_dac_power();
|
||||
if (dac_power.has_value()) {
|
||||
if (this->dac_output_select_->has_index(dac_power.value())) {
|
||||
this->dac_output_select_->publish_state(dac_power.value());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown DAC output power value: %d", dac_power.value());
|
||||
if (this->dac_output_initial_.has_value()) {
|
||||
const DacOutputLine line = this->dac_output_initial_.value();
|
||||
ES8388_ERROR_FAILED(this->set_dac_output(line));
|
||||
this->dac_output_select_->publish_state(static_cast<size_t>(line));
|
||||
} else {
|
||||
auto dac_power = this->get_dac_power();
|
||||
if (dac_power.has_value()) {
|
||||
if (this->dac_output_select_->has_index(dac_power.value())) {
|
||||
this->dac_output_select_->publish_state(dac_power.value());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown DAC output power value: %d", dac_power.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->adc_input_mic_select_ != nullptr) {
|
||||
auto mic_input = this->get_mic_input();
|
||||
if (mic_input.has_value()) {
|
||||
if (this->adc_input_mic_select_->has_index(mic_input.value())) {
|
||||
this->adc_input_mic_select_->publish_state(mic_input.value());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown ADC input mic value: %d", mic_input.value());
|
||||
if (this->adc_input_mic_initial_.has_value()) {
|
||||
const AdcInputMicLine line = this->adc_input_mic_initial_.value();
|
||||
ES8388_ERROR_FAILED(this->set_adc_input_mic(line));
|
||||
this->adc_input_mic_select_->publish_state(static_cast<size_t>(line));
|
||||
} else {
|
||||
auto mic_input = this->get_mic_input();
|
||||
if (mic_input.has_value()) {
|
||||
if (this->adc_input_mic_select_->has_index(mic_input.value())) {
|
||||
this->adc_input_mic_select_->publish_state(mic_input.value());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown ADC input mic value: %d", mic_input.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +150,7 @@ void ES8388::setup() {
|
||||
void ES8388::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ES8388 Audio Codec:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
#ifdef USE_SELECT
|
||||
#ifdef USE_ES8388_SELECT
|
||||
LOG_SELECT(" ", "DacOutputSelect", this->dac_output_select_);
|
||||
LOG_SELECT(" ", "ADCInputMicSelect", this->adc_input_mic_select_);
|
||||
#endif
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "esphome/components/audio_dac/audio_dac.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_SELECT
|
||||
#ifdef USE_ES8388_SELECT
|
||||
#include "esphome/components/select/select.h"
|
||||
#endif
|
||||
|
||||
@@ -26,7 +26,7 @@ enum AdcInputMicLine : uint8_t {
|
||||
};
|
||||
|
||||
class ES8388 final : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
|
||||
#ifdef USE_SELECT
|
||||
#ifdef USE_ES8388_SELECT
|
||||
SUB_SELECT(dac_output)
|
||||
SUB_SELECT(adc_input_mic)
|
||||
#endif
|
||||
@@ -68,11 +68,22 @@ class ES8388 final : public audio_dac::AudioDac, public Component, public i2c::I
|
||||
bool set_dac_output(DacOutputLine line);
|
||||
bool set_adc_input_mic(AdcInputMicLine line);
|
||||
|
||||
#ifdef USE_ES8388_SELECT
|
||||
/// Line to apply in setup() instead of keeping what the codec reports.
|
||||
void set_dac_output_initial(DacOutputLine line) { this->dac_output_initial_ = line; }
|
||||
void set_adc_input_mic_initial(AdcInputMicLine line) { this->adc_input_mic_initial_ = line; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
/// @brief Mutes or unmutes the DAC audio out
|
||||
/// @param mute_state True to mute, false to unmute
|
||||
/// @return True if successful and false otherwise
|
||||
bool set_mute_state_(bool mute_state);
|
||||
|
||||
#ifdef USE_ES8388_SELECT
|
||||
optional<DacOutputLine> dac_output_initial_{};
|
||||
optional<AdcInputMicLine> adc_input_mic_initial_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::es8388
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import select
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_CHIP # noqa: F401
|
||||
from esphome.const import (
|
||||
CONF_INITIAL_OPTION,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_CHIP, # noqa: F401
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..audio_dac import CONF_ES8388_ID, ES8388, es8388_ns
|
||||
@@ -9,6 +13,21 @@ from ..audio_dac import CONF_ES8388_ID, ES8388, es8388_ns
|
||||
CONF_DAC_OUTPUT = "dac_output"
|
||||
CONF_ADC_INPUT_MIC = "adc_input_mic"
|
||||
|
||||
DacOutputLine = es8388_ns.enum("DacOutputLine")
|
||||
DAC_OUTPUT_OPTIONS = {
|
||||
"LINE1": DacOutputLine.DAC_OUTPUT_LINE1,
|
||||
"LINE2": DacOutputLine.DAC_OUTPUT_LINE2,
|
||||
"BOTH": DacOutputLine.DAC_OUTPUT_BOTH,
|
||||
}
|
||||
DAC_OUTPUT_OPTION_NAMES = list(DAC_OUTPUT_OPTIONS)
|
||||
AdcInputMicLine = es8388_ns.enum("AdcInputMicLine")
|
||||
ADC_INPUT_MIC_OPTIONS = {
|
||||
"LINE1": AdcInputMicLine.ADC_INPUT_MIC_LINE1,
|
||||
"LINE2": AdcInputMicLine.ADC_INPUT_MIC_LINE2,
|
||||
"DIFFERENCE": AdcInputMicLine.ADC_INPUT_MIC_DIFFERENCE,
|
||||
}
|
||||
ADC_INPUT_MIC_OPTION_NAMES = list(ADC_INPUT_MIC_OPTIONS)
|
||||
|
||||
DacOutputSelect = es8388_ns.class_("DacOutputSelect", select.Select)
|
||||
ADCInputMicSelect = es8388_ns.class_("ADCInputMicSelect", select.Select)
|
||||
|
||||
@@ -19,24 +38,45 @@ CONFIG_SCHEMA = cv.All(
|
||||
DacOutputSelect,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_CHIP,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_INITIAL_OPTION): cv.enum(
|
||||
DAC_OUTPUT_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ADC_INPUT_MIC): select.select_schema(
|
||||
ADCInputMicSelect,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_CHIP,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_INITIAL_OPTION): cv.enum(
|
||||
ADC_INPUT_MIC_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_ES8388_SELECT")
|
||||
hub = await cg.get_variable(config[CONF_ES8388_ID])
|
||||
selects = select.sub_selects(config, parent=hub)
|
||||
await selects(
|
||||
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=["LINE1", "LINE2", "BOTH"]
|
||||
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=DAC_OUTPUT_OPTION_NAMES
|
||||
)
|
||||
await selects(
|
||||
CONF_ADC_INPUT_MIC,
|
||||
hub.set_adc_input_mic_select,
|
||||
options=["LINE1", "LINE2", "DIFFERENCE"],
|
||||
options=ADC_INPUT_MIC_OPTION_NAMES,
|
||||
)
|
||||
if (
|
||||
initial := config.get(CONF_DAC_OUTPUT, {}).get(CONF_INITIAL_OPTION)
|
||||
) is not None:
|
||||
cg.add(hub.set_dac_output_initial(initial))
|
||||
if (
|
||||
initial := config.get(CONF_ADC_INPUT_MIC, {}).get(CONF_INITIAL_OPTION)
|
||||
) is not None:
|
||||
cg.add(hub.set_adc_input_mic_initial(initial))
|
||||
|
||||
@@ -131,6 +131,7 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
void ESP32Preferences::open() {
|
||||
// Runs from app_main() before the logger is initialized; any logging here
|
||||
// must be deferred. See s_open_err and make_preference() below.
|
||||
// Components that need NVS check nvs_handle instead of initializing it again.
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err == 0)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#ifndef CONFIG_BT_CONTROLLER_DISABLED
|
||||
#include <esp_bt.h>
|
||||
@@ -28,7 +29,6 @@ extern "C" {
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
|
||||
@@ -77,8 +77,9 @@ static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
|
||||
|
||||
void ESP32BLE::setup() {
|
||||
global_ble = this;
|
||||
if (!ble_pre_setup_()) {
|
||||
ESP_LOGE(TAG, "BLE could not be prepared for configuration");
|
||||
// Preferences opens NVS in app_main(); a zero handle means it could not be opened, even after an erase
|
||||
if (global_preferences->nvs_handle == 0) {
|
||||
ESP_LOGE(TAG, "NVS unavailable");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -185,15 +186,6 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ESP32BLE::ble_pre_setup_() {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void ESP32BLE::advertising_init_() {
|
||||
if (this->advertising_ != nullptr)
|
||||
|
||||
@@ -185,7 +185,6 @@ class ESP32BLE final : public Component {
|
||||
}
|
||||
this->ble_events_.get_and_reset_dropped_count();
|
||||
}
|
||||
bool ble_pre_setup_();
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void advertising_init_();
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <nvs_flash.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <esp_bt_main.h>
|
||||
#ifndef CONFIG_BT_CONTROLLER_DISABLED
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <cinttypes>
|
||||
|
||||
#ifdef USE_OTA
|
||||
|
||||
@@ -282,7 +282,7 @@ void ESP32Camera::loop() {
|
||||
}
|
||||
this->frame_count_++;
|
||||
if (now - this->last_log_time_ >= FRAME_LOG_INTERVAL_MS) {
|
||||
ESP_LOGD(TAG, "Received %u images in last %us", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000);
|
||||
ESP_LOGD(TAG, "Received %u images in last %" PRIu32 "s", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000);
|
||||
this->last_log_time_ = now;
|
||||
this->frame_count_ = 0;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import esphome.codegen as cg
|
||||
from esphome.components import canbus
|
||||
from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed
|
||||
from esphome.components.esp32 import (
|
||||
CONF_MINIMUM_CHIP_REVISION,
|
||||
KEY_ESP32,
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
@@ -20,6 +22,8 @@ from esphome.components.esp32 import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADVANCED,
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_RX_PIN,
|
||||
@@ -27,6 +31,7 @@ from esphome.const import (
|
||||
CONF_TX_PIN,
|
||||
CONF_TX_QUEUE_LEN,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@Sympatron"]
|
||||
@@ -42,6 +47,7 @@ CanMode = esp32_can_ns.enum("CanMode")
|
||||
CAN_MODES = {
|
||||
"NORMAL": CanMode.CAN_MODE_NORMAL,
|
||||
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
|
||||
"NO_ACK": CanMode.CAN_MODE_NO_ACK,
|
||||
}
|
||||
|
||||
# Currently the driver only supports a subset of the bit rates defined in canbus
|
||||
@@ -49,6 +55,7 @@ CAN_MODES = {
|
||||
# See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI)
|
||||
|
||||
CAN_SPEEDS_ESP32 = {
|
||||
"20KBPS": CanSpeed.CAN_20KBPS,
|
||||
"25KBPS": CanSpeed.CAN_25KBPS,
|
||||
"50KBPS": CanSpeed.CAN_50KBPS,
|
||||
"100KBPS": CanSpeed.CAN_100KBPS,
|
||||
@@ -114,6 +121,22 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if get_esp32_variant() == VARIANT_ESP32 and config[CONF_BIT_RATE] == "20KBPS":
|
||||
esp32_config = fv.full_config.get()[KEY_ESP32]
|
||||
min_revision = esp32_config[CONF_FRAMEWORK][CONF_ADVANCED].get(
|
||||
CONF_MINIMUM_CHIP_REVISION
|
||||
)
|
||||
if min_revision is None or min_revision < "2.0":
|
||||
raise cv.Invalid(
|
||||
"20KBPS on ESP32 requires esp32.framework.advanced.minimum_chip_revision: '2.0' or higher",
|
||||
path=[CONF_BIT_RATE],
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def get_default_tx_enqueue_timeout(bit_rate: str) -> int:
|
||||
bit_rate_numeric = canbus.get_rate(bit_rate)
|
||||
bits_per_packet = 140 # ~max CAN message length
|
||||
|
||||
@@ -33,6 +33,10 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
|
||||
case canbus::CAN_16KBPS:
|
||||
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_16KBITS();
|
||||
return true;
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S3) || (defined(USE_ESP32_VARIANT_ESP32) && CONFIG_ESP32_REV_MIN_FULL >= 200)
|
||||
case canbus::CAN_20KBPS:
|
||||
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_20KBITS();
|
||||
return true;
|
||||
@@ -75,10 +79,13 @@ bool ESP32Can::setup_internal() {
|
||||
}
|
||||
|
||||
// Select TWAI mode based on configuration
|
||||
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
|
||||
|
||||
twai_mode_t twai_mode = TWAI_MODE_NORMAL;
|
||||
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
|
||||
twai_mode = TWAI_MODE_LISTEN_ONLY;
|
||||
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
|
||||
} else if (this->mode_ == CAN_MODE_NO_ACK) {
|
||||
twai_mode = TWAI_MODE_NO_ACK;
|
||||
ESP_LOGI(TAG, "CAN bus configured in NO_ACK mode (transmit without requiring ACK)");
|
||||
}
|
||||
|
||||
twai_general_config_t g_config =
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace esphome::esp32_can {
|
||||
enum CanMode : uint8_t {
|
||||
CAN_MODE_NORMAL = 0,
|
||||
CAN_MODE_LISTEN_ONLY = 1,
|
||||
CAN_MODE_NO_ACK = 2,
|
||||
};
|
||||
|
||||
class ESP32Can final : public canbus::Canbus {
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <deque>
|
||||
|
||||
namespace esphome::ezo {
|
||||
|
||||
static const char *const TAG = "ezo.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "ezo.sensor");
|
||||
|
||||
enum EzoCommandType : uint8_t {
|
||||
EZO_READ = 0,
|
||||
|
||||
@@ -330,7 +330,9 @@ async def write_image(
|
||||
if byte_order is not None:
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
pixels = encoder.convert(
|
||||
image.resize((width, height)), path
|
||||
).get_flattened_data()
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@tuct"]
|
||||
DOMAIN = "filter_lifetime"
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "filter_lifetime.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::filter_lifetime {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "filter_lifetime");
|
||||
|
||||
static constexpr uint32_t SECONDS_PER_HOUR = 3600;
|
||||
static constexpr float MINUTES_PER_DAY = 24.0f * 60.0f;
|
||||
|
||||
void FilterLifetime::setup() {
|
||||
this->pref_ = this->make_entity_preference<uint32_t>();
|
||||
this->pref_.load(&this->runtime_seconds_);
|
||||
this->last_update_ = millis();
|
||||
this->publish_values_();
|
||||
}
|
||||
|
||||
void FilterLifetime::update() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const uint32_t elapsed_ms = now - this->last_update_;
|
||||
this->last_update_ = now;
|
||||
|
||||
if (this->get_is_on_()) {
|
||||
float speed = this->get_current_speed_();
|
||||
if (std::isnan(speed))
|
||||
speed = 0.0f;
|
||||
speed = std::clamp(speed, 0.0f, 100.0f);
|
||||
const uint32_t prev_hours = this->runtime_seconds_ / SECONDS_PER_HOUR;
|
||||
this->carry_ms_ += static_cast<uint32_t>(elapsed_ms * (speed / 100.0f));
|
||||
this->runtime_seconds_ += this->carry_ms_ / 1000;
|
||||
this->carry_ms_ %= 1000;
|
||||
// Save once per hour of runtime (and in on_shutdown): every save erases a flash sector on ESP8266.
|
||||
if (this->runtime_seconds_ / SECONDS_PER_HOUR != prev_hours)
|
||||
this->pref_.save(&this->runtime_seconds_);
|
||||
ESP_LOGV(TAG, "Elapsed %" PRIu32 " ms at %.0f%%, runtime %" PRIu32 " s", elapsed_ms, speed, this->runtime_seconds_);
|
||||
}
|
||||
this->publish_values_();
|
||||
}
|
||||
|
||||
void FilterLifetime::publish_values_() {
|
||||
const float runtime_minutes = this->runtime_seconds_ / 60.0f;
|
||||
const float max_minutes = this->max_lifetime_minutes_;
|
||||
const float remaining_minutes = std::max(0.0f, max_minutes - runtime_minutes);
|
||||
this->publish_state(100.0f * remaining_minutes / max_minutes);
|
||||
if (this->runtime_hours_sensor_ != nullptr)
|
||||
this->runtime_hours_sensor_->publish_state(runtime_minutes / 60.0f);
|
||||
if (this->remaining_days_sensor_ != nullptr)
|
||||
this->remaining_days_sensor_->publish_state(remaining_minutes / MINUTES_PER_DAY);
|
||||
}
|
||||
|
||||
void FilterLifetime::on_shutdown() { this->pref_.save(&this->runtime_seconds_); }
|
||||
|
||||
void FilterLifetime::reset_filter() {
|
||||
this->runtime_seconds_ = 0;
|
||||
this->carry_ms_ = 0;
|
||||
this->pref_.save(&this->runtime_seconds_);
|
||||
this->publish_values_();
|
||||
ESP_LOGI(TAG, "Filter lifetime reset");
|
||||
}
|
||||
|
||||
bool FilterLifetime::get_is_on_() {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// Reads false until the sensor first publishes, so nothing accumulates before then.
|
||||
if (this->is_on_sensor_ != nullptr)
|
||||
return this->is_on_sensor_->state;
|
||||
#endif
|
||||
if (this->is_on_lambda_ != nullptr)
|
||||
return this->is_on_lambda_();
|
||||
return true;
|
||||
}
|
||||
|
||||
float FilterLifetime::get_current_speed_() {
|
||||
if (this->current_speed_sensor_ != nullptr)
|
||||
return this->current_speed_sensor_->state; // NaN before the first publish, treated as 0
|
||||
if (this->current_speed_lambda_ != nullptr)
|
||||
return this->current_speed_lambda_();
|
||||
return 100.0f;
|
||||
}
|
||||
|
||||
void FilterLifetime::dump_config() {
|
||||
LOG_SENSOR("", "Filter Lifetime", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Max lifetime: %" PRIu32 " min (%.1f days)\n"
|
||||
" Runtime: %.1f h",
|
||||
this->max_lifetime_minutes_, this->max_lifetime_minutes_ / MINUTES_PER_DAY,
|
||||
this->runtime_seconds_ / 3600.0f);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->is_on_sensor_ != nullptr) {
|
||||
LOG_BINARY_SENSOR(" ", "Is-on sensor", this->is_on_sensor_);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ESP_LOGCONFIG(TAG, " Is-on: %s",
|
||||
this->is_on_lambda_ != nullptr ? LOG_STR_LITERAL("lambda") : LOG_STR_LITERAL("always"));
|
||||
}
|
||||
if (this->current_speed_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Speed sensor", this->current_speed_sensor_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Speed: %s",
|
||||
this->current_speed_lambda_ != nullptr ? LOG_STR_LITERAL("lambda") : LOG_STR_LITERAL("100%"));
|
||||
}
|
||||
LOG_SENSOR(" ", "Runtime hours", this->runtime_hours_sensor_);
|
||||
LOG_SENSOR(" ", "Remaining days", this->remaining_days_sensor_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
} // namespace esphome::filter_lifetime
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::filter_lifetime {
|
||||
|
||||
class FilterLifetime : public sensor::Sensor, public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
void reset_filter();
|
||||
|
||||
void set_max_lifetime(uint32_t minutes) { this->max_lifetime_minutes_ = minutes; }
|
||||
void set_is_on_lambda(bool (*is_on)()) { this->is_on_lambda_ = is_on; }
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void set_is_on_sensor(binary_sensor::BinarySensor *sensor) { this->is_on_sensor_ = sensor; }
|
||||
#endif
|
||||
void set_current_speed_lambda(float (*speed)()) { this->current_speed_lambda_ = speed; }
|
||||
void set_current_speed_sensor(sensor::Sensor *sensor) { this->current_speed_sensor_ = sensor; }
|
||||
void set_runtime_hours_sensor(sensor::Sensor *sensor) { this->runtime_hours_sensor_ = sensor; }
|
||||
void set_remaining_days_sensor(sensor::Sensor *sensor) { this->remaining_days_sensor_ = sensor; }
|
||||
|
||||
protected:
|
||||
bool get_is_on_();
|
||||
float get_current_speed_();
|
||||
void publish_values_();
|
||||
|
||||
bool (*is_on_lambda_)(){nullptr};
|
||||
float (*current_speed_lambda_)(){nullptr};
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
binary_sensor::BinarySensor *is_on_sensor_{nullptr};
|
||||
#endif
|
||||
sensor::Sensor *current_speed_sensor_{nullptr};
|
||||
sensor::Sensor *runtime_hours_sensor_{nullptr};
|
||||
sensor::Sensor *remaining_days_sensor_{nullptr};
|
||||
ESPPreferenceObject pref_;
|
||||
uint32_t max_lifetime_minutes_{0};
|
||||
uint32_t last_update_{0};
|
||||
uint32_t runtime_seconds_{0}; // Speed-weighted running time, persisted
|
||||
uint32_t carry_ms_{0}; // Weighted milliseconds not yet counted as a second
|
||||
};
|
||||
|
||||
} // namespace esphome::filter_lifetime
|
||||
@@ -0,0 +1,94 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_DURATION,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_HOUR,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_MAX_LIFETIME = "max_lifetime"
|
||||
CONF_IS_ON = "is_on"
|
||||
CONF_IS_ON_SENSOR = "is_on_sensor"
|
||||
CONF_CURRENT_SPEED = "current_speed"
|
||||
CONF_CURRENT_SPEED_SENSOR = "current_speed_sensor"
|
||||
CONF_RUNTIME_HOURS = "runtime_hours"
|
||||
CONF_REMAINING_DAYS = "remaining_days"
|
||||
UNIT_DAY = "d"
|
||||
|
||||
filter_lifetime_ns = cg.esphome_ns.namespace("filter_lifetime")
|
||||
FilterLifetime = filter_lifetime_ns.class_(
|
||||
"FilterLifetime", sensor.Sensor, cg.PollingComponent
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(
|
||||
FilterLifetime,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=2,
|
||||
icon="mdi:air-filter",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_MAX_LIFETIME): cv.All(
|
||||
cv.positive_time_period_minutes,
|
||||
cv.Range(min=cv.TimePeriod(minutes=1)),
|
||||
),
|
||||
cv.Optional(CONF_IS_ON): cv.returning_lambda,
|
||||
cv.Optional(CONF_IS_ON_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_CURRENT_SPEED): cv.returning_lambda,
|
||||
cv.Optional(CONF_CURRENT_SPEED_SENSOR): cv.use_id(sensor.Sensor),
|
||||
cv.Optional(CONF_RUNTIME_HOURS): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HOUR,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
icon="mdi:timer-outline",
|
||||
),
|
||||
cv.Optional(CONF_REMAINING_DAYS): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_DAY,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
icon="mdi:calendar-clock",
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s")),
|
||||
cv.has_at_most_one_key(CONF_IS_ON, CONF_IS_ON_SENSOR),
|
||||
cv.has_at_most_one_key(CONF_CURRENT_SPEED, CONF_CURRENT_SPEED_SENSOR),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_max_lifetime(config[CONF_MAX_LIFETIME]))
|
||||
|
||||
if (is_on := config.get(CONF_IS_ON)) is not None:
|
||||
cg.add(var.set_is_on_lambda(await cg.templatable(is_on, [], bool)))
|
||||
elif (is_on_sensor := config.get(CONF_IS_ON_SENSOR)) is not None:
|
||||
cg.add(var.set_is_on_sensor(await cg.get_variable(is_on_sensor)))
|
||||
|
||||
if (current_speed := config.get(CONF_CURRENT_SPEED)) is not None:
|
||||
cg.add(
|
||||
var.set_current_speed_lambda(await cg.templatable(current_speed, [], float))
|
||||
)
|
||||
elif (speed_sensor := config.get(CONF_CURRENT_SPEED_SENSOR)) is not None:
|
||||
cg.add(var.set_current_speed_sensor(await cg.get_variable(speed_sensor)))
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_RUNTIME_HOURS, var.set_runtime_hours_sensor)
|
||||
await sensors(CONF_REMAINING_DAYS, var.set_remaining_days_sensor)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"filter_lifetime.reset_filter",
|
||||
cv.Schema({cv.GenerateID(): cv.use_id(FilterLifetime)}),
|
||||
automation.ApplyCall("reset_filter()"),
|
||||
)
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "hon_climate.h"
|
||||
#include "hon_packet.h"
|
||||
|
||||
@@ -20,6 +21,27 @@ constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
|
||||
const uint8_t ONE_BUF[] = {0x00, 0x01};
|
||||
const uint8_t ZERO_BUF[] = {0x00, 0x00};
|
||||
|
||||
// Alarm texts indexed by alarm code; the last entry is the fallback for unknown codes
|
||||
PROGMEM_STRING_TABLE(
|
||||
HonAlarmMessages, "Outdoor module failure", "Outdoor defrost sensor failure",
|
||||
"Outdoor compressor exhaust sensor failure", "Outdoor EEPROM abnormality", "Indoor coil sensor failure",
|
||||
"Indoor-outdoor communication failure", "Power supply overvoltage protection",
|
||||
"Communication failure between panel and indoor unit", "Outdoor compressor overheat protection",
|
||||
"Outdoor environmental sensor abnormality", "Full water protection", "Indoor EEPROM failure",
|
||||
"Outdoor out air sensor failure", "CBD and module communication failure", "Indoor DC fan failure",
|
||||
"Outdoor DC fan failure", "Door switch failure", "Dust filter needs cleaning reminder", "Water shortage protection",
|
||||
"Humidity sensor failure", "Indoor temperature sensor failure", "Manipulator limit failure",
|
||||
"Indoor PM2.5 sensor failure", "Outdoor PM2.5 sensor failure", "Indoor heating overload/high load alarm",
|
||||
"Outdoor AC current protection", "Outdoor compressor operation abnormality", "Outdoor DC current protection",
|
||||
"Outdoor no-load failure", "CT current abnormality", "Indoor cooling freeze protection",
|
||||
"High and low pressure protection", "Compressor out air temperature is too high",
|
||||
"Outdoor evaporator sensor failure", "Outdoor cooling overload", "Water pump drainage failure",
|
||||
"Three-phase power supply failure", "Four-way valve failure", "External alarm/scraper flow switch failure",
|
||||
"Temperature cutoff protection alarm", "Different mode operation failure", "Electronic expansion valve failure",
|
||||
"Dual heat source sensor Tw failure", "Communication failure with the wired controller",
|
||||
"Indoor unit address duplication failure", "50Hz zero crossing failure", "Outdoor unit failure",
|
||||
"Formaldehyde sensor failure", "VOC sensor failure", "CO2 sensor failure", "Firewall failure", "Unknown");
|
||||
|
||||
HonClimate::HonClimate()
|
||||
: cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), active_alarms_{0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00,
|
||||
@@ -687,9 +709,17 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
|
||||
if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) {
|
||||
bool alarm_status = (packet[2 + i] & alarm_bit) != 0;
|
||||
int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO;
|
||||
const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT
|
||||
? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str()
|
||||
: "Unknown";
|
||||
#ifdef USE_ESP8266
|
||||
// Static so a delayed on_alarm automation can still read it, but only until the next
|
||||
// alarm change, including a later one in this packet; `code` stays correct
|
||||
static char alarm_message[HonAlarmMessages::MAX_LENGTH + 1];
|
||||
ESPHOME_strncpy_P(alarm_message,
|
||||
reinterpret_cast<const char *>(
|
||||
HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX)),
|
||||
sizeof(alarm_message));
|
||||
#else
|
||||
const char *alarm_message = HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX);
|
||||
#endif
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated",
|
||||
alarm_code, alarm_message);
|
||||
if (alarm_status) {
|
||||
|
||||
@@ -197,60 +197,4 @@ enum class SubcommandsControl : uint16_t {
|
||||
// content: all values like in status packet)
|
||||
};
|
||||
|
||||
const std::string HON_ALARM_MESSAGES[] = {
|
||||
"Outdoor module failure",
|
||||
"Outdoor defrost sensor failure",
|
||||
"Outdoor compressor exhaust sensor failure",
|
||||
"Outdoor EEPROM abnormality",
|
||||
"Indoor coil sensor failure",
|
||||
"Indoor-outdoor communication failure",
|
||||
"Power supply overvoltage protection",
|
||||
"Communication failure between panel and indoor unit",
|
||||
"Outdoor compressor overheat protection",
|
||||
"Outdoor environmental sensor abnormality",
|
||||
"Full water protection",
|
||||
"Indoor EEPROM failure",
|
||||
"Outdoor out air sensor failure",
|
||||
"CBD and module communication failure",
|
||||
"Indoor DC fan failure",
|
||||
"Outdoor DC fan failure",
|
||||
"Door switch failure",
|
||||
"Dust filter needs cleaning reminder",
|
||||
"Water shortage protection",
|
||||
"Humidity sensor failure",
|
||||
"Indoor temperature sensor failure",
|
||||
"Manipulator limit failure",
|
||||
"Indoor PM2.5 sensor failure",
|
||||
"Outdoor PM2.5 sensor failure",
|
||||
"Indoor heating overload/high load alarm",
|
||||
"Outdoor AC current protection",
|
||||
"Outdoor compressor operation abnormality",
|
||||
"Outdoor DC current protection",
|
||||
"Outdoor no-load failure",
|
||||
"CT current abnormality",
|
||||
"Indoor cooling freeze protection",
|
||||
"High and low pressure protection",
|
||||
"Compressor out air temperature is too high",
|
||||
"Outdoor evaporator sensor failure",
|
||||
"Outdoor cooling overload",
|
||||
"Water pump drainage failure",
|
||||
"Three-phase power supply failure",
|
||||
"Four-way valve failure",
|
||||
"External alarm/scraper flow switch failure",
|
||||
"Temperature cutoff protection alarm",
|
||||
"Different mode operation failure",
|
||||
"Electronic expansion valve failure",
|
||||
"Dual heat source sensor Tw failure",
|
||||
"Communication failure with the wired controller",
|
||||
"Indoor unit address duplication failure",
|
||||
"50Hz zero crossing failure",
|
||||
"Outdoor unit failure",
|
||||
"Formaldehyde sensor failure",
|
||||
"VOC sensor failure",
|
||||
"CO2 sensor failure",
|
||||
"Firewall failure",
|
||||
};
|
||||
|
||||
constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]);
|
||||
|
||||
} // namespace esphome::haier::hon_protocol
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
DOMAIN = "haier_tundra"
|
||||
CODEOWNERS = ["@fauxpark"]
|
||||
@@ -0,0 +1,43 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_LIGHT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..climate import HaierTundra, haier_tundra_ns
|
||||
|
||||
CODEOWNERS = ["@fauxpark"]
|
||||
|
||||
HaierTundraButton = haier_tundra_ns.class_(
|
||||
"HaierTundraButton", button.Button, cg.Component
|
||||
)
|
||||
ButtonKind = haier_tundra_ns.enum("ButtonKind", is_class=True)
|
||||
|
||||
ICON_LED_OUTLINE = "mdi:led-outline"
|
||||
ICON_SPRAY_BOTTLE = "mdi:spray-bottle"
|
||||
|
||||
CONF_HAIER_TUNDRA_ID = "haier_tundra_id"
|
||||
CONF_SELF_CLEAN = "self_clean"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_HAIER_TUNDRA_ID): cv.use_id(HaierTundra),
|
||||
cv.Optional(CONF_LIGHT): button.button_schema(
|
||||
HaierTundraButton, icon=ICON_LED_OUTLINE
|
||||
),
|
||||
cv.Optional(CONF_SELF_CLEAN): button.button_schema(
|
||||
HaierTundraButton, icon=ICON_SPRAY_BOTTLE
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_HAIER_TUNDRA_ID])
|
||||
for key, kind in (
|
||||
(CONF_LIGHT, ButtonKind.BUTTON_KIND_LIGHT),
|
||||
(CONF_SELF_CLEAN, ButtonKind.BUTTON_KIND_SELF_CLEAN),
|
||||
):
|
||||
if (conf := config.get(key)) is not None:
|
||||
var = await button.new_button(conf, hub, kind)
|
||||
await cg.register_component(var, conf)
|
||||
@@ -0,0 +1,18 @@
|
||||
#include "haier_tundra_button.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::haier_tundra {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "haier_tundra.button");
|
||||
|
||||
void HaierTundraButton::dump_config() { LOG_BUTTON("", "Haier Tundra Button", this); }
|
||||
|
||||
void HaierTundraButton::press_action() {
|
||||
if (this->kind_ == ButtonKind::BUTTON_KIND_LIGHT) {
|
||||
this->parent_->toggle_light();
|
||||
} else {
|
||||
this->parent_->start_self_clean();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::haier_tundra
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "esphome/components/haier_tundra/haier_tundra.h"
|
||||
|
||||
namespace esphome::haier_tundra {
|
||||
|
||||
enum class ButtonKind : uint8_t {
|
||||
BUTTON_KIND_LIGHT,
|
||||
BUTTON_KIND_SELF_CLEAN,
|
||||
};
|
||||
|
||||
class HaierTundraButton final : public button::Button, public Component, public Parented<HaierTundra> {
|
||||
public:
|
||||
HaierTundraButton(HaierTundra *parent, ButtonKind kind) : Parented(parent), kind_(kind) {}
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
|
||||
ButtonKind kind_;
|
||||
};
|
||||
|
||||
} // namespace esphome::haier_tundra
|
||||
@@ -0,0 +1,51 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir, remote_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PLATFORM
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
|
||||
haier_tundra_ns = cg.esphome_ns.namespace("haier_tundra")
|
||||
HaierTundra = haier_tundra_ns.class_("HaierTundra", climate_ir.ClimateIR)
|
||||
|
||||
CONF_UNIT = "unit"
|
||||
Unit = haier_tundra_ns.enum("Unit")
|
||||
UNITS = {
|
||||
"a": Unit.HAIER_HEADER_UNIT_A,
|
||||
"b": Unit.HAIER_HEADER_UNIT_B,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HaierTundra).extend(
|
||||
{
|
||||
cv.Optional(CONF_UNIT, default="a"): cv.enum(UNITS, lower=True),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _final_validate_unit(config: ConfigType) -> None:
|
||||
# Each transmitter may drive at most one unit A and one unit B
|
||||
climates = fv.full_config.get()["climate"]
|
||||
seen = set()
|
||||
|
||||
for climate in climates:
|
||||
if CONF_PLATFORM in climate and climate[CONF_PLATFORM] == "haier_tundra":
|
||||
unit_tx_pair = (
|
||||
climate[CONF_UNIT],
|
||||
climate[remote_base.CONF_TRANSMITTER_ID],
|
||||
)
|
||||
|
||||
if unit_tx_pair in seen and climate[CONF_ID] == config[CONF_ID]:
|
||||
raise cv.Invalid(
|
||||
"Only one of each unit is allowed per transmitter", path=[CONF_UNIT]
|
||||
)
|
||||
seen.add(unit_tx_pair)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate_unit
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
remote_base.request_protocol("haier")
|
||||
await climate_ir.new_climate_ir(config, config[CONF_UNIT])
|
||||
@@ -0,0 +1,260 @@
|
||||
#include "haier_tundra.h"
|
||||
|
||||
namespace esphome::haier_tundra {
|
||||
|
||||
// Number of bytes in decoded packet sans checksum
|
||||
static constexpr uint8_t HAIER_TUNDRA_FRAME_SIZE = 13;
|
||||
|
||||
void HaierTundra::transmit_state() {
|
||||
// Button value does not seem to matter too much except
|
||||
// when toggling light, as it has no bit flag of its own
|
||||
// So in the default case we can simply set it to POWER
|
||||
this->transmit_(HAIER_BUTTON_POWER);
|
||||
}
|
||||
|
||||
void HaierTundra::transmit_(uint8_t button) {
|
||||
// Force Quiet and Turbo off if we're not in Cool or Heat mode
|
||||
if (this->mode != climate::CLIMATE_MODE_COOL && this->mode != climate::CLIMATE_MODE_HEAT) {
|
||||
#ifdef USE_SWITCH
|
||||
if (this->quiet_mode_switch_ != nullptr) {
|
||||
this->quiet_mode_switch_->publish_state(false);
|
||||
}
|
||||
if (this->turbo_mode_switch_ != nullptr) {
|
||||
this->turbo_mode_switch_->publish_state(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
this->quiet_mode_ = false;
|
||||
this->turbo_mode_ = false;
|
||||
}
|
||||
|
||||
const uint8_t frame[HAIER_TUNDRA_FRAME_SIZE] = {
|
||||
this->unit_,
|
||||
static_cast<uint8_t>(this->temperature_() << 4 | this->vertical_swing_()),
|
||||
static_cast<uint8_t>(this->horizontal_swing_() << 5),
|
||||
static_cast<uint8_t>(this->health_() << 1),
|
||||
static_cast<uint8_t>(this->power_() << 6),
|
||||
static_cast<uint8_t>(this->fan_speed_() << 5),
|
||||
static_cast<uint8_t>(this->quiet_() << 7 | this->turbo_() << 6),
|
||||
static_cast<uint8_t>(this->operation_mode_() << 5),
|
||||
0x00,
|
||||
0x00,
|
||||
static_cast<uint8_t>((button == HAIER_BUTTON_SELF_CLEAN ? 1 : 0) << 4),
|
||||
0x00,
|
||||
button,
|
||||
};
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
remote_base::HaierProtocol().encode(transmit.get_data(), frame, sizeof(frame));
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
uint8_t HaierTundra::temperature_() {
|
||||
return (uint8_t) roundf(clamp<float>(this->target_temperature, HAIER_TEMP_MIN, HAIER_TEMP_MAX)) - HAIER_TEMP_MIN;
|
||||
}
|
||||
|
||||
uint8_t HaierTundra::vertical_swing_() {
|
||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||
return HAIER_SWING_V_OFF;
|
||||
}
|
||||
switch (this->swing_mode) {
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
return HAIER_SWING_V_AUTO;
|
||||
default:
|
||||
if (this->mode == climate::CLIMATE_MODE_HEAT) {
|
||||
return HAIER_SWING_V_1BOT;
|
||||
}
|
||||
return HAIER_SWING_V_1TOP; // Cool/Dry/Auto
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t HaierTundra::horizontal_swing_() {
|
||||
switch (this->swing_mode) {
|
||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
return HAIER_SWING_H_AUTO;
|
||||
default:
|
||||
return HAIER_SWING_H_MIDDLE;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t HaierTundra::health_() { return this->health_mode_ ? 1 : 0; }
|
||||
|
||||
uint8_t HaierTundra::power_() { return this->mode == climate::CLIMATE_MODE_OFF ? 0 : 1; }
|
||||
|
||||
uint8_t HaierTundra::fan_speed_() {
|
||||
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
return HAIER_FAN_LOW;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
return HAIER_FAN_MED;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
return HAIER_FAN_HIGH;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
return HAIER_FAN_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t HaierTundra::quiet_() { return this->quiet_mode_ ? 1 : 0; }
|
||||
|
||||
uint8_t HaierTundra::turbo_() { return this->turbo_mode_ ? 1 : 0; }
|
||||
|
||||
uint8_t HaierTundra::operation_mode_() {
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
return HAIER_MODE_COOL;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
return HAIER_MODE_DRY;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
return HAIER_MODE_HEAT;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
return HAIER_MODE_FAN;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
default:
|
||||
return HAIER_MODE_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
bool HaierTundra::on_receive(remote_base::RemoteReceiveData data) {
|
||||
auto haier = remote_base::HaierProtocol().decode(data);
|
||||
if (haier.has_value()) {
|
||||
return this->on_haier_(*haier);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HaierTundra::on_haier_(const remote_base::HaierData &haier) {
|
||||
// Verify frame length and header
|
||||
if (haier.data.size() != HAIER_TUNDRA_FRAME_SIZE || haier.data[0] != this->unit_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->target_temperature = this->get_temperature_(haier.data[1] >> 4);
|
||||
this->swing_mode = this->get_swing_mode_(haier.data[1] & 0xF, haier.data[2] >> 5);
|
||||
this->health_mode_ = haier.data[3] & 0x2;
|
||||
this->mode = this->get_operation_mode_(haier.data[4] & 0x40, haier.data[7] >> 5);
|
||||
this->fan_mode = this->get_fan_speed_(haier.data[5] >> 5);
|
||||
this->quiet_mode_ = haier.data[6] & 0x80;
|
||||
this->turbo_mode_ = haier.data[6] & 0x40;
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
if (this->health_mode_switch_ != nullptr) {
|
||||
this->health_mode_switch_->publish_state(this->health_mode_);
|
||||
}
|
||||
if (this->quiet_mode_switch_ != nullptr) {
|
||||
this->quiet_mode_switch_->publish_state(this->quiet_mode_);
|
||||
}
|
||||
if (this->turbo_mode_switch_ != nullptr) {
|
||||
this->turbo_mode_switch_->publish_state(this->turbo_mode_);
|
||||
}
|
||||
#endif
|
||||
|
||||
this->publish_state();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
float HaierTundra::get_temperature_(uint8_t temp) { return temp + HAIER_TEMP_MIN; }
|
||||
|
||||
climate::ClimateSwingMode HaierTundra::get_swing_mode_(uint8_t swing_v, uint8_t swing_h) {
|
||||
if (swing_v == HAIER_SWING_V_AUTO) {
|
||||
if (swing_h == HAIER_SWING_H_AUTO) {
|
||||
return climate::CLIMATE_SWING_BOTH;
|
||||
}
|
||||
|
||||
return climate::CLIMATE_SWING_VERTICAL;
|
||||
} else if (swing_h == HAIER_SWING_H_AUTO) {
|
||||
return climate::CLIMATE_SWING_HORIZONTAL;
|
||||
}
|
||||
|
||||
return climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
|
||||
climate::ClimateMode HaierTundra::get_operation_mode_(uint8_t power, uint8_t mode) {
|
||||
if (!power) {
|
||||
return climate::CLIMATE_MODE_OFF;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case HAIER_MODE_COOL:
|
||||
return climate::CLIMATE_MODE_COOL;
|
||||
case HAIER_MODE_DRY:
|
||||
return climate::CLIMATE_MODE_DRY;
|
||||
case HAIER_MODE_HEAT:
|
||||
return climate::CLIMATE_MODE_HEAT;
|
||||
case HAIER_MODE_FAN:
|
||||
return climate::CLIMATE_MODE_FAN_ONLY;
|
||||
case HAIER_MODE_AUTO:
|
||||
default:
|
||||
return climate::CLIMATE_MODE_HEAT_COOL;
|
||||
}
|
||||
}
|
||||
|
||||
climate::ClimateFanMode HaierTundra::get_fan_speed_(uint8_t fan) {
|
||||
switch (fan) {
|
||||
case HAIER_FAN_LOW:
|
||||
return climate::CLIMATE_FAN_LOW;
|
||||
case HAIER_FAN_MED:
|
||||
return climate::CLIMATE_FAN_MEDIUM;
|
||||
case HAIER_FAN_HIGH:
|
||||
return climate::CLIMATE_FAN_HIGH;
|
||||
case HAIER_FAN_AUTO:
|
||||
default:
|
||||
return climate::CLIMATE_FAN_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
void HaierTundra::set_health_mode(bool health, bool transmit) {
|
||||
this->health_mode_ = health;
|
||||
|
||||
if (transmit && this->mode != climate::CLIMATE_MODE_OFF) {
|
||||
this->transmit_(HAIER_BUTTON_HEALTH);
|
||||
}
|
||||
}
|
||||
|
||||
void HaierTundra::set_quiet_mode(bool quiet, bool transmit) {
|
||||
// Quiet & Turbo cannot be enabled at the same time
|
||||
if (quiet) {
|
||||
#ifdef USE_SWITCH
|
||||
if (this->turbo_mode_switch_ != nullptr) {
|
||||
this->turbo_mode_switch_->publish_state(false);
|
||||
}
|
||||
#endif
|
||||
this->turbo_mode_ = false;
|
||||
}
|
||||
|
||||
this->quiet_mode_ = quiet;
|
||||
if (transmit)
|
||||
this->transmit_(HAIER_BUTTON_TURBO_QUIET);
|
||||
}
|
||||
|
||||
void HaierTundra::set_turbo_mode(bool turbo, bool transmit) {
|
||||
// Quiet & Turbo cannot be enabled at the same time
|
||||
if (turbo) {
|
||||
#ifdef USE_SWITCH
|
||||
if (this->quiet_mode_switch_ != nullptr) {
|
||||
this->quiet_mode_switch_->publish_state(false);
|
||||
}
|
||||
#endif
|
||||
this->quiet_mode_ = false;
|
||||
}
|
||||
|
||||
this->turbo_mode_ = turbo;
|
||||
if (transmit)
|
||||
this->transmit_(HAIER_BUTTON_TURBO_QUIET);
|
||||
}
|
||||
|
||||
void HaierTundra::toggle_light() {
|
||||
if (this->mode != climate::CLIMATE_MODE_OFF) {
|
||||
this->transmit_(HAIER_BUTTON_LIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
void HaierTundra::start_self_clean() {
|
||||
// Can be started when off
|
||||
this->transmit_(HAIER_BUTTON_SELF_CLEAN);
|
||||
}
|
||||
|
||||
} // namespace esphome::haier_tundra
|
||||
@@ -0,0 +1,139 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
#include "esphome/components/remote_base/haier_protocol.h"
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::haier_tundra {
|
||||
|
||||
// Byte 0 - Header value (Unit ID)
|
||||
enum Unit : uint8_t {
|
||||
HAIER_HEADER_UNIT_B = 0x59,
|
||||
HAIER_HEADER_UNIT_A = 0xA6,
|
||||
};
|
||||
|
||||
// Byte 1[7:4] - Temperature (˚C) 0x0..0xE
|
||||
static constexpr uint8_t HAIER_TEMP_MIN = 16;
|
||||
static constexpr uint8_t HAIER_TEMP_MAX = 30;
|
||||
|
||||
// Byte 1[3:0] - Vertical Swing
|
||||
static constexpr uint8_t HAIER_SWING_V_OFF = 0x0; // Power off value; from Auto, stops at current position
|
||||
static constexpr uint8_t HAIER_SWING_V_2TOP = 0x1; // [|/ ]
|
||||
static constexpr uint8_t HAIER_SWING_V_1TOP = 0x2; // [| ] (Not available in Heat mode)
|
||||
static constexpr uint8_t HAIER_SWING_V_2BOT = 0x3; // [ //]
|
||||
static constexpr uint8_t HAIER_SWING_V_1BOT = 0xA; // [ /] (Heat mode only)
|
||||
static constexpr uint8_t HAIER_SWING_V_AUTO = 0xC;
|
||||
|
||||
// Byte 2[7:5] - Horizontal Swing
|
||||
static constexpr uint8_t HAIER_SWING_H_MIDDLE = 0x0; // [ || ]
|
||||
static constexpr uint8_t HAIER_SWING_H_LEFT = 0x3; // [/ ]
|
||||
static constexpr uint8_t HAIER_SWING_H_MIDDLE_LEFT = 0x4; // [ | ]
|
||||
static constexpr uint8_t HAIER_SWING_H_MIDDLE_RIGHT = 0x5; // [ | ]
|
||||
static constexpr uint8_t HAIER_SWING_H_RIGHT = 0x6; // [ \]
|
||||
static constexpr uint8_t HAIER_SWING_H_AUTO = 0x7; // [/||\]
|
||||
|
||||
// Byte 3[7:5] - Timer Mode
|
||||
static constexpr uint8_t HAIER_TIMER_MODE_DISABLED = 0x0;
|
||||
static constexpr uint8_t HAIER_TIMER_MODE_OFF = 0x1; // OFF
|
||||
static constexpr uint8_t HAIER_TIMER_MODE_ON = 0x2; // ON
|
||||
static constexpr uint8_t HAIER_TIMER_MODE_ON_OFF = 0x4; // ON → OFF
|
||||
static constexpr uint8_t HAIER_TIMER_MODE_OFF_ON = 0x5; // ON ← OFF
|
||||
|
||||
// Byte 3[1] - Health
|
||||
// Byte 4[6] - Power
|
||||
|
||||
// Byte 5[7:5] - Fan Speed
|
||||
static constexpr uint8_t HAIER_FAN_HIGH = 0x1;
|
||||
static constexpr uint8_t HAIER_FAN_MED = 0x2;
|
||||
static constexpr uint8_t HAIER_FAN_LOW = 0x3;
|
||||
static constexpr uint8_t HAIER_FAN_AUTO = 0x5;
|
||||
|
||||
// Byte 5[4:0] - Timer Off Hours (0-23) 0x00..0x17
|
||||
// Byte 6[7] - Quiet Mode
|
||||
// Byte 6[6] - Turbo Mode
|
||||
// Byte 6[5:0] - Timer Off Minutes (0-59) 0x00..0x3B
|
||||
|
||||
// Byte 7[7:5] - Mode
|
||||
static constexpr uint8_t HAIER_MODE_AUTO = 0x0;
|
||||
static constexpr uint8_t HAIER_MODE_COOL = 0x1;
|
||||
static constexpr uint8_t HAIER_MODE_DRY = 0x2;
|
||||
static constexpr uint8_t HAIER_MODE_HEAT = 0x4;
|
||||
static constexpr uint8_t HAIER_MODE_FAN = 0x6;
|
||||
|
||||
// Byte 7[4:0] - Timer On Hours (0-23) 0x00..0x17
|
||||
// Byte 8[7] - Sleep Mode
|
||||
// Byte 8[5:0] - Timer On Minutes (0-59) 0x00..0x3B
|
||||
// Byte 10[4] - Self Clean
|
||||
|
||||
// Byte 12 - Button
|
||||
static constexpr uint8_t HAIER_BUTTON_TEMP_UP = 0x00;
|
||||
static constexpr uint8_t HAIER_BUTTON_TEMP_DOWN = 0x01;
|
||||
static constexpr uint8_t HAIER_BUTTON_SWING_V = 0x02;
|
||||
static constexpr uint8_t HAIER_BUTTON_SWING_H = 0x03;
|
||||
static constexpr uint8_t HAIER_BUTTON_FAN_SPEED = 0x04;
|
||||
static constexpr uint8_t HAIER_BUTTON_POWER = 0x05;
|
||||
static constexpr uint8_t HAIER_BUTTON_MODE = 0x06;
|
||||
static constexpr uint8_t HAIER_BUTTON_HEALTH = 0x07;
|
||||
static constexpr uint8_t HAIER_BUTTON_TURBO_QUIET = 0x08;
|
||||
static constexpr uint8_t HAIER_BUTTON_SLEEP = 0x0B;
|
||||
static constexpr uint8_t HAIER_BUTTON_TIMER = 0x10;
|
||||
static constexpr uint8_t HAIER_BUTTON_LIGHT = 0x15;
|
||||
static constexpr uint8_t HAIER_BUTTON_SELF_CLEAN = 0x19;
|
||||
|
||||
class HaierTundra final : public climate_ir::ClimateIR {
|
||||
public:
|
||||
explicit HaierTundra(Unit unit)
|
||||
: climate_ir::ClimateIR(HAIER_TEMP_MIN, HAIER_TEMP_MAX, 1.0f, true, true,
|
||||
{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}),
|
||||
unit_(unit) {}
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
SUB_SWITCH(health_mode);
|
||||
SUB_SWITCH(quiet_mode);
|
||||
SUB_SWITCH(turbo_mode);
|
||||
#endif
|
||||
|
||||
void set_health_mode(bool health, bool transmit = true);
|
||||
void set_quiet_mode(bool quiet, bool transmit = true);
|
||||
void set_turbo_mode(bool turbo, bool transmit = true);
|
||||
|
||||
void toggle_light();
|
||||
void start_self_clean();
|
||||
|
||||
protected:
|
||||
// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
|
||||
void transmit_(uint8_t button);
|
||||
uint8_t temperature_();
|
||||
uint8_t vertical_swing_();
|
||||
uint8_t horizontal_swing_();
|
||||
uint8_t health_();
|
||||
uint8_t power_();
|
||||
uint8_t fan_speed_();
|
||||
uint8_t quiet_();
|
||||
uint8_t turbo_();
|
||||
uint8_t operation_mode_();
|
||||
|
||||
// Handle received IR Buffer
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
bool on_haier_(const remote_base::HaierData &haier);
|
||||
|
||||
float get_temperature_(uint8_t temp);
|
||||
climate::ClimateSwingMode get_swing_mode_(uint8_t swing_v, uint8_t swing_h);
|
||||
climate::ClimateMode get_operation_mode_(uint8_t power, uint8_t mode);
|
||||
climate::ClimateFanMode get_fan_speed_(uint8_t fan);
|
||||
|
||||
Unit unit_;
|
||||
bool health_mode_{false};
|
||||
bool quiet_mode_{false};
|
||||
bool turbo_mode_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::haier_tundra
|
||||
@@ -0,0 +1,60 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..climate import HaierTundra, haier_tundra_ns
|
||||
|
||||
CODEOWNERS = ["@fauxpark"]
|
||||
|
||||
HaierTundraSwitch = haier_tundra_ns.class_(
|
||||
"HaierTundraSwitch", switch.Switch, cg.Component
|
||||
)
|
||||
SwitchKind = haier_tundra_ns.enum("SwitchKind", is_class=True)
|
||||
|
||||
ICON_LEAF = "mdi:leaf"
|
||||
ICON_ARROW_DOWN_RIGHT = "mdi:arrow-down-right"
|
||||
ICON_ARROW_UP_RIGHT_BOLD = "mdi:arrow-up-right-bold"
|
||||
|
||||
CONF_HAIER_TUNDRA_ID = "haier_tundra_id"
|
||||
CONF_HEALTH_MODE = "health_mode"
|
||||
CONF_QUIET_MODE = "quiet_mode"
|
||||
CONF_TURBO_MODE = "turbo_mode"
|
||||
|
||||
|
||||
def _switch_schema(icon: str) -> cv.Schema:
|
||||
return switch.switch_schema(
|
||||
HaierTundraSwitch,
|
||||
icon=icon,
|
||||
default_restore_mode="RESTORE_DEFAULT_OFF",
|
||||
device_class=DEVICE_CLASS_SWITCH,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_HAIER_TUNDRA_ID): cv.use_id(HaierTundra),
|
||||
cv.Optional(CONF_HEALTH_MODE): _switch_schema(ICON_LEAF),
|
||||
cv.Optional(CONF_QUIET_MODE): _switch_schema(ICON_ARROW_DOWN_RIGHT),
|
||||
cv.Optional(CONF_TURBO_MODE): _switch_schema(ICON_ARROW_UP_RIGHT_BOLD),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_HAIER_TUNDRA_ID])
|
||||
switches = switch.sub_switches(config)
|
||||
if sw := await switches(
|
||||
CONF_HEALTH_MODE, hub.set_health_mode_switch, hub, SwitchKind.SWITCH_KIND_HEALTH
|
||||
):
|
||||
await cg.register_component(sw, config[CONF_HEALTH_MODE])
|
||||
if sw := await switches(
|
||||
CONF_QUIET_MODE, hub.set_quiet_mode_switch, hub, SwitchKind.SWITCH_KIND_QUIET
|
||||
):
|
||||
await cg.register_component(sw, config[CONF_QUIET_MODE])
|
||||
if sw := await switches(
|
||||
CONF_TURBO_MODE, hub.set_turbo_mode_switch, hub, SwitchKind.SWITCH_KIND_TURBO
|
||||
):
|
||||
await cg.register_component(sw, config[CONF_TURBO_MODE])
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "haier_tundra_switch.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::haier_tundra {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "haier_tundra.switch");
|
||||
|
||||
void HaierTundraSwitch::setup() {
|
||||
auto initial = this->get_initial_state_with_restore_mode();
|
||||
if (initial.has_value()) {
|
||||
const bool state = initial.value();
|
||||
this->apply_(state, false);
|
||||
this->publish_state(state);
|
||||
}
|
||||
}
|
||||
|
||||
void HaierTundraSwitch::dump_config() { LOG_SWITCH("", "Haier Tundra Switch", this); }
|
||||
|
||||
void HaierTundraSwitch::write_state(bool state) {
|
||||
// Quiet and Turbo can only be turned on in Cool or Heat mode
|
||||
if (state && this->kind_ != SwitchKind::SWITCH_KIND_HEALTH && this->parent_->mode != climate::CLIMATE_MODE_COOL &&
|
||||
this->parent_->mode != climate::CLIMATE_MODE_HEAT) {
|
||||
return;
|
||||
}
|
||||
this->apply_(state, true);
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
void HaierTundraSwitch::apply_(bool state, bool transmit) {
|
||||
switch (this->kind_) {
|
||||
case SwitchKind::SWITCH_KIND_HEALTH:
|
||||
this->parent_->set_health_mode(state, transmit);
|
||||
break;
|
||||
case SwitchKind::SWITCH_KIND_QUIET:
|
||||
this->parent_->set_quiet_mode(state, transmit);
|
||||
break;
|
||||
case SwitchKind::SWITCH_KIND_TURBO:
|
||||
this->parent_->set_turbo_mode(state, transmit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::haier_tundra
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "esphome/components/haier_tundra/haier_tundra.h"
|
||||
|
||||
namespace esphome::haier_tundra {
|
||||
|
||||
enum class SwitchKind : uint8_t {
|
||||
SWITCH_KIND_HEALTH,
|
||||
SWITCH_KIND_QUIET,
|
||||
SWITCH_KIND_TURBO,
|
||||
};
|
||||
|
||||
class HaierTundraSwitch final : public switch_::Switch, public Component, public Parented<HaierTundra> {
|
||||
public:
|
||||
HaierTundraSwitch(HaierTundra *parent, SwitchKind kind) : Parented(parent), kind_(kind) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
void apply_(bool state, bool transmit);
|
||||
|
||||
SwitchKind kind_;
|
||||
};
|
||||
|
||||
} // namespace esphome::haier_tundra
|
||||
@@ -126,8 +126,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
cg.add(var.set_protocol(config[CONF_PROTOCOL]))
|
||||
tag = heatpumpir_ns.ProtocolTag.template(config[CONF_PROTOCOL])
|
||||
var = await climate_ir.new_climate_ir(config, tag())
|
||||
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
|
||||
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
|
||||
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <IRSender.h>
|
||||
#include <HeatpumpIRFactory.h>
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
@@ -45,71 +44,76 @@ class IRSenderESPHome : public IRSender {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "heatpumpir.climate");
|
||||
|
||||
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {
|
||||
{PROTOCOL_AUX, []() { return new AUXHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_BALLU, []() { return new BalluHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_CARRIER_MCA, []() { return new CarrierMCAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_CARRIER_NQV, []() { return new CarrierNQVHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAP, []() { return new GreeYAPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_IVT, []() { return new IVTHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MIDEA, []() { return new MideaHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FA, []() { return new MitsubishiFAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FD, []() { return new MitsubishiFDHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FE, []() { return new MitsubishiFEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_FDTC, []() { return new MitsubishiHeavyFDTCHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZJ, []() { return new MitsubishiHeavyZJHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZM, []() { return new MitsubishiHeavyZMHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZMP, []() { return new MitsubishiHeavyZMPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_KJ, []() { return new MitsubishiKJHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_MSC, []() { return new MitsubishiMSCHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_MSY, []() { return new MitsubishiMSYHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_AQV, []() { return new SamsungAQVHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_FJM, []() { return new SamsungFJMHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_NIBE, []() { return new NibeHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_QLIMA_1, []() { return new Qlima1HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_QLIMA_2, []() { return new Qlima2HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_AQV12MSAN, []() { return new SamsungAQV12MSANHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ZHJG01, []() { return new ZHJG01HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT
|
||||
};
|
||||
template<Protocol P> struct ProtocolClass;
|
||||
|
||||
template<Protocol P> HeatpumpIR *create_heatpump_ir() {
|
||||
return new typename ProtocolClass<P>::type(); // NOLINT
|
||||
}
|
||||
|
||||
// Codegen calls only the configured protocols, so the linker drops the other classes
|
||||
#define HEATPUMPIR_PROTOCOL(protocol, cls) \
|
||||
template<> struct ProtocolClass<protocol> { using type = cls; }; \
|
||||
template HeatpumpIR *create_heatpump_ir<protocol>();
|
||||
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_AUX, AUXHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_BALLU, BalluHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_MCA, CarrierMCAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_NQV, CarrierNQVHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC417, DaikinHeatpumpARC417IR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC480, DaikinHeatpumpARC480A14IR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN, DaikinHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ELECTROLUXYAL, ElectroluxYALHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_FUEGO, FuegoHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_FUJITSU_AWYZ, FujitsuHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREE, GreeGenericHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAA, GreeYAAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAN, GreeYANHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAC, GreeYACHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYT, GreeYTHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAP, GreeYAPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HISENSE_AUD, HisenseHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HITACHI, HitachiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HYUNDAI, HyundaiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_IVT, IVTHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MIDEA, MideaHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FA, MitsubishiFAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FD, MitsubishiFDHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FE, MitsubishiFEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_FDTC, MitsubishiHeavyFDTCHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZJ, MitsubishiHeavyZJHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZM, MitsubishiHeavyZMHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZMP, MitsubishiHeavyZMPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_KJ, MitsubishiKJHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSC, MitsubishiMSCHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSY, MitsubishiMSYHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_SEZ, MitsubishiSEZKDXXHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_CKP, PanasonicCKPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_DKE, PanasonicDKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_EKE, PanasonicEKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_JKE, PanasonicJKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_LKE, PanasonicLKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_NKE, PanasonicNKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV, SamsungAQVHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_FJM, SamsungFJMHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SHARP, SharpHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA_DAISEIKAI, ToshibaDaiseikaiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA, ToshibaHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHLT01, ZHLT01HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_NIBE, NibeHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_1, Qlima1HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_2, Qlima2HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV12MSAN, SamsungAQV12MSANHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHJG01, ZHJG01HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_AIRWAY, AIRWAYHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_BGH_AUD, BGHHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_ALTDKE, PanasonicAltDKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PHILCO_PHS32, PhilcoPHS32HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_VAILLANTVAI8, VaillantHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_R51M, R51MHeatpumpIR)
|
||||
|
||||
#undef HEATPUMPIR_PROTOCOL
|
||||
|
||||
void HeatpumpIRClimate::setup() {
|
||||
auto protocol_constructor = PROTOCOL_CONSTRUCTOR_MAP.find(protocol_);
|
||||
if (protocol_constructor == PROTOCOL_CONSTRUCTOR_MAP.end()) {
|
||||
ESP_LOGE(TAG, "Invalid protocol");
|
||||
return;
|
||||
}
|
||||
this->heatpump_ir_ = protocol_constructor->second();
|
||||
climate_ir::ClimateIR::setup();
|
||||
if (this->sensor_) {
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
|
||||
@@ -89,25 +89,21 @@ enum VerticalDirection {
|
||||
VERTICAL_DIRECTION_DOWN = 5,
|
||||
};
|
||||
|
||||
/// Creates the HeatpumpIR library class for a protocol; instantiated in heatpumpir.cpp.
|
||||
template<Protocol P> HeatpumpIR *create_heatpump_ir();
|
||||
|
||||
/// Selects the protocol at compile time so the enum and the library object always match.
|
||||
template<Protocol P> struct ProtocolTag {};
|
||||
|
||||
// Temperature
|
||||
const float TEMP_MIN = 0; // Celsius
|
||||
const float TEMP_MAX = 100; // Celsius
|
||||
|
||||
class HeatpumpIRClimate final : public climate_ir::ClimateIR {
|
||||
public:
|
||||
HeatpumpIRClimate()
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_FAN_AUTO},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||
template<Protocol P>
|
||||
explicit HeatpumpIRClimate(ProtocolTag<P> /*tag*/) : HeatpumpIRClimate(P, create_heatpump_ir<P>()) {}
|
||||
void setup() override;
|
||||
void set_protocol(Protocol protocol) {
|
||||
this->protocol_ = protocol;
|
||||
if (this->has_fan_byte_presets_()) {
|
||||
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
|
||||
}
|
||||
}
|
||||
void set_horizontal_default(HorizontalDirection horizontal_direction) {
|
||||
this->default_horizontal_direction_ = horizontal_direction;
|
||||
}
|
||||
@@ -119,6 +115,18 @@ class HeatpumpIRClimate final : public climate_ir::ClimateIR {
|
||||
void set_min_temperature(float temperature) { this->min_temperature_ = temperature; }
|
||||
|
||||
protected:
|
||||
HeatpumpIRClimate(Protocol protocol, HeatpumpIR *heatpump_ir)
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_FAN_AUTO},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}),
|
||||
heatpump_ir_(heatpump_ir),
|
||||
protocol_(protocol) {
|
||||
if (this->has_fan_byte_presets_()) {
|
||||
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
|
||||
}
|
||||
}
|
||||
HeatpumpIR *heatpump_ir_;
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
/// Transmit via IR the state of this climate controller.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
from esphome.components.motion import MotionComponent
|
||||
|
||||
CODEOWNERS = ["@lboue"]
|
||||
DOMAIN = "icm20689"
|
||||
|
||||
CONF_ICM20689_ID = "icm20689_id"
|
||||
# C++ namespace / class
|
||||
icm20689_ns = cg.esphome_ns.namespace("icm20689")
|
||||
ICM20689Component = icm20689_ns.class_(
|
||||
"ICM20689Component", MotionComponent, spi.SPIDevice
|
||||
)
|
||||
@@ -0,0 +1,126 @@
|
||||
#include "icm20689.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::icm20689 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "icm20689");
|
||||
|
||||
namespace {
|
||||
|
||||
// Range enum values 0..3 map to full-scale settings of 2x/4x/8x/16x the base range.
|
||||
constexpr float accel_scale(uint8_t range) { return (2.0f * (1U << range)) / 32768.0f; }
|
||||
|
||||
constexpr float gyro_scale(uint8_t range) { return (250.0f * (1U << range)) / 32768.0f; }
|
||||
|
||||
} // namespace
|
||||
|
||||
void ICM20689Component::write_register_(uint8_t reg, uint8_t value) {
|
||||
this->enable();
|
||||
this->write_byte(reg & 0x7F);
|
||||
this->write_byte(value);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void ICM20689Component::read_registers_(uint8_t reg, uint8_t *data, size_t len) {
|
||||
this->enable();
|
||||
this->write_byte(reg | ICM20689_READ_BIT);
|
||||
this->read_array(data, len);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void ICM20689Component::setup() {
|
||||
MotionComponent::setup();
|
||||
|
||||
this->spi_setup();
|
||||
|
||||
if (!this->spi_is_ready()) {
|
||||
ESP_LOGE(TAG, "SPI device is not ready");
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Datasheet 4.20: a soft reset must be the first operation after power-up,
|
||||
// before any register initialization. WHO_AM_I is only valid afterwards.
|
||||
this->write_register_(ICM20689_REG_PWR_MGMT_1, ICM20689_SOFT_RESET);
|
||||
delay(100); // NOLINT
|
||||
|
||||
// The chip comes out of reset in sleep mode: wake it and select the
|
||||
// auto-selected best clock source (required for full gyro performance).
|
||||
this->write_register_(ICM20689_REG_PWR_MGMT_1, ICM20689_CLKSEL_AUTO);
|
||||
|
||||
// USER_CTRL is not writable in sleep mode, so I2C_IF_DIS is set once awake.
|
||||
// Datasheet 6.1: disable the I2C interface when using SPI.
|
||||
this->write_register_(ICM20689_REG_USER_CTRL, ICM20689_I2C_IF_DIS);
|
||||
|
||||
uint8_t who_am_i = 0;
|
||||
this->read_registers_(ICM20689_REG_WHO_AM_I, &who_am_i, 1);
|
||||
|
||||
if (who_am_i != ICM20689_WHO_AM_I_VALUE) {
|
||||
ESP_LOGE(TAG, "Unexpected WHO_AM_I 0x%02X (expected 0x%02X) -- check wiring/CS", who_am_i, ICM20689_WHO_AM_I_VALUE);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow the sensor to settle after waking from sleep.
|
||||
delay(50); // NOLINT
|
||||
|
||||
// Configure the accelerometer and gyroscope full-scale ranges.
|
||||
this->write_register_(ICM20689_REG_ACCEL_CONFIG, static_cast<uint8_t>(this->accel_range_) << 3);
|
||||
this->write_register_(ICM20689_REG_GYRO_CONFIG, static_cast<uint8_t>(this->gyro_range_) << 3);
|
||||
}
|
||||
|
||||
void ICM20689Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ICM20689 IMU:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, " " ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Accel range: ±%dg", 2 << static_cast<uint8_t>(this->accel_range_));
|
||||
ESP_LOGCONFIG(TAG, " Gyro range: ±%d°/s", 250 << static_cast<uint8_t>(this->gyro_range_));
|
||||
|
||||
MotionComponent::dump_config();
|
||||
}
|
||||
|
||||
bool ICM20689Component::update_data(motion::MotionData &data) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
const uint8_t accel_range = static_cast<uint8_t>(this->accel_range_);
|
||||
const uint8_t gyro_range = static_cast<uint8_t>(this->gyro_range_);
|
||||
|
||||
// Read one contiguous sample:
|
||||
// 0..5 accelerometer X/Y/Z
|
||||
// 6..7 temperature
|
||||
// 8..13 gyroscope X/Y/Z
|
||||
uint8_t raw_data[14] = {};
|
||||
this->read_registers_(ICM20689_REG_ACCEL_XOUT_H, raw_data, sizeof(raw_data));
|
||||
|
||||
const float accel_factor = accel_scale(accel_range);
|
||||
|
||||
data.acceleration[motion::X_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[0], raw_data[1])) * accel_factor;
|
||||
data.acceleration[motion::Y_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[2], raw_data[3])) * accel_factor;
|
||||
data.acceleration[motion::Z_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[4], raw_data[5])) * accel_factor;
|
||||
|
||||
const float gyro_factor = gyro_scale(gyro_range);
|
||||
|
||||
data.angular_rate[motion::X_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[8], raw_data[9])) * gyro_factor;
|
||||
data.angular_rate[motion::Y_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[10], raw_data[11])) * gyro_factor;
|
||||
data.angular_rate[motion::Z_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[12], raw_data[13])) * gyro_factor;
|
||||
|
||||
if (!this->temperature_callback_.empty()) {
|
||||
// Datasheet formula: temperature (°C) = raw / 326.8 + 25.
|
||||
const float temperature = static_cast<int16_t>(encode_uint16(raw_data[6], raw_data[7])) / 326.8f + 25.0f;
|
||||
this->temperature_callback_.call(temperature);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::icm20689
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/motion/motion_component.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::icm20689 {
|
||||
|
||||
// Register map
|
||||
static constexpr uint8_t ICM20689_REG_WHO_AM_I = 0x75;
|
||||
static constexpr uint8_t ICM20689_REG_PWR_MGMT_1 = 0x6B;
|
||||
static constexpr uint8_t ICM20689_REG_USER_CTRL = 0x6A;
|
||||
static constexpr uint8_t ICM20689_REG_GYRO_CONFIG = 0x1B;
|
||||
static constexpr uint8_t ICM20689_REG_ACCEL_CONFIG = 0x1C;
|
||||
static constexpr uint8_t ICM20689_REG_ACCEL_XOUT_H = 0x3B;
|
||||
static constexpr uint8_t ICM20689_READ_BIT = 0x80;
|
||||
static constexpr uint8_t ICM20689_WHO_AM_I_VALUE = 0x98;
|
||||
static constexpr uint8_t ICM20689_SOFT_RESET = 0x81; // PWR_MGMT_1: DEVICE_RESET | CLKSEL=1 (datasheet 4.20)
|
||||
static constexpr uint8_t ICM20689_CLKSEL_AUTO = 0x01; // PWR_MGMT_1: SLEEP=0, CLKSEL=1 (auto-select best clock)
|
||||
static constexpr uint8_t ICM20689_I2C_IF_DIS = 0x10; // USER_CTRL bit 4
|
||||
|
||||
// Accelerometer range options: FS_SEL bits [4:3] of ACCEL_CONFIG
|
||||
enum ICM20689AccelRange : uint8_t {
|
||||
ICM20689_ACCEL_RANGE_2G = 0x00,
|
||||
ICM20689_ACCEL_RANGE_4G = 0x01,
|
||||
ICM20689_ACCEL_RANGE_8G = 0x02,
|
||||
ICM20689_ACCEL_RANGE_16G = 0x03,
|
||||
};
|
||||
|
||||
// Gyroscope range options: FS_SEL bits [4:3] of GYRO_CONFIG
|
||||
enum ICM20689GyroRange : uint8_t {
|
||||
ICM20689_GYRO_RANGE_250 = 0x00,
|
||||
ICM20689_GYRO_RANGE_500 = 0x01,
|
||||
ICM20689_GYRO_RANGE_1000 = 0x02,
|
||||
ICM20689_GYRO_RANGE_2000 = 0x03,
|
||||
};
|
||||
|
||||
class ICM20689Component final : public motion::MotionComponent,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_accel_range(ICM20689AccelRange range) { this->accel_range_ = range; }
|
||||
void set_gyro_range(ICM20689GyroRange range) { this->gyro_range_ = range; }
|
||||
|
||||
template<typename F> void add_temperature_listener(F &&callback) {
|
||||
this->temperature_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
protected:
|
||||
bool update_data(motion::MotionData &data) override;
|
||||
|
||||
void write_register_(uint8_t reg, uint8_t value);
|
||||
// Reads len bytes starting at reg in one CS-low transaction.
|
||||
void read_registers_(uint8_t reg, uint8_t *data, size_t len);
|
||||
|
||||
ICM20689AccelRange accel_range_{ICM20689_ACCEL_RANGE_2G};
|
||||
ICM20689GyroRange gyro_range_{ICM20689_GYRO_RANGE_250};
|
||||
|
||||
LazyCallbackManager<void(float)> temperature_callback_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::icm20689
|
||||
@@ -0,0 +1,56 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
from esphome.components.const import CONF_ACCELEROMETER_RANGE, CONF_GYROSCOPE_RANGE
|
||||
from esphome.components.motion import motion_schema, new_motion_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ICM20689Component, icm20689_ns
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
# Enum proxies (must match the C++ enum values exactly)
|
||||
ICM20689AccelRange = icm20689_ns.enum("ICM20689AccelRange")
|
||||
ACCEL_RANGE_OPTIONS = {
|
||||
"2G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_2G,
|
||||
"4G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_4G,
|
||||
"8G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_8G,
|
||||
"16G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_16G,
|
||||
}
|
||||
|
||||
ICM20689GyroRange = icm20689_ns.enum("ICM20689GyroRange")
|
||||
GYRO_RANGE_OPTIONS = {
|
||||
"250DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_250,
|
||||
"500DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_500,
|
||||
"1000DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_1000,
|
||||
"2000DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_2000,
|
||||
}
|
||||
|
||||
# Top-level CONFIG_SCHEMA
|
||||
CONFIG_SCHEMA = (
|
||||
motion_schema(ICM20689Component, has_accel=True, has_gyro=True)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_ACCELEROMETER_RANGE, default="2G"): cv.enum(
|
||||
ACCEL_RANGE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_GYROSCOPE_RANGE, default="250DPS"): cv.enum(
|
||||
GYRO_RANGE_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
spi.spi_device_schema(
|
||||
cs_pin_required=True, default_mode="MODE0", default_data_rate="1MHz"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Code generation
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await new_motion_component(config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
cg.add(var.set_accel_range(config[CONF_ACCELEROMETER_RANGE]))
|
||||
cg.add(var.set_gyro_range(config[CONF_GYROSCOPE_RANGE]))
|
||||
@@ -0,0 +1,42 @@
|
||||
# YAML config keys
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_THERMOMETER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_ICM20689_ID, ICM20689Component
|
||||
|
||||
AUTO_LOAD = ["icm20689"]
|
||||
|
||||
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(
|
||||
{
|
||||
cv.Optional(CONF_TYPE): cv.one_of(CONF_TEMPERATURE),
|
||||
cv.GenerateID(CONF_ICM20689_ID): cv.use_id(ICM20689Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
parent = await cg.get_variable(config[CONF_ICM20689_ID])
|
||||
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))
|
||||
@@ -3,4 +3,5 @@ DOMAIN = "ili9xxx"
|
||||
DEPRECATED_COMPONENT = """
|
||||
The 'ili9xxx' component is deprecated and no new models will be added to it.
|
||||
New model PRs should target the newer and more performant 'mipi_spi' component.
|
||||
It will be removed in ESPHome 2027.5.0.
|
||||
"""
|
||||
|
||||
@@ -220,8 +220,10 @@ FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# Remove before 2027.5.0
|
||||
LOGGER.warning(
|
||||
"The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead."
|
||||
"The 'ili9xxx' component is deprecated and will be removed in ESPHome 2027.5.0, "
|
||||
"migrate to 'mipi_spi' instead."
|
||||
)
|
||||
rhs = MODELS[config[CONF_MODEL]].new()
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "ili9xxx_defines.h"
|
||||
#include "ili9xxx_init.h"
|
||||
|
||||
namespace esphome::ili9xxx {
|
||||
|
||||
static const char *const TAG = "ili9xxx";
|
||||
ESPHOME_LOG_TAG(TAG, "ili9xxx");
|
||||
const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6
|
||||
|
||||
enum ILI9XXXColorMode {
|
||||
|
||||
@@ -145,13 +145,42 @@ def is_alpha_only(image: Image):
|
||||
return all(b.getextrema()[1] == 0 for b in image.split()[:-1])
|
||||
|
||||
|
||||
class ImageBinary(ImageEncoder):
|
||||
class BitPackedEncoder(ImageEncoder):
|
||||
"""Packs pixels MSB first, with every row padded to a whole byte."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bpp: int,
|
||||
width: int,
|
||||
height: int,
|
||||
transparency: str,
|
||||
dither: Image.Dither,
|
||||
invert_alpha: bool,
|
||||
):
|
||||
self.bpp = bpp
|
||||
super().__init__(
|
||||
(width * bpp + 7) // 8, height, transparency, dither, invert_alpha
|
||||
)
|
||||
self.bitno = 0
|
||||
|
||||
def add_bit(self, bit: bool) -> None:
|
||||
if bit:
|
||||
self.data[self.index] |= 0x80 >> self.bitno
|
||||
self.bitno += 1
|
||||
if self.bitno == 8:
|
||||
self.end_row()
|
||||
|
||||
def end_row(self) -> None:
|
||||
if self.bitno != 0:
|
||||
self.bitno = 0
|
||||
self.index += 1
|
||||
|
||||
|
||||
class ImageBinary(BitPackedEncoder):
|
||||
allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY}
|
||||
|
||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
||||
self.width8 = (width + 7) // 8
|
||||
super().__init__(self.width8, height, transparency, dither, invert_alpha)
|
||||
self.bitno = 0
|
||||
super().__init__(1, width, height, transparency, dither, invert_alpha)
|
||||
|
||||
def convert(self, image, path):
|
||||
if is_alpha_only(image):
|
||||
@@ -159,22 +188,46 @@ class ImageBinary(ImageEncoder):
|
||||
return image.convert("1", dither=self.dither)
|
||||
|
||||
def encode(self, pixel):
|
||||
if self.invert_alpha:
|
||||
pixel = not pixel
|
||||
if pixel:
|
||||
self.data[self.index] |= 0x80 >> (self.bitno % 8)
|
||||
self.bitno += 1
|
||||
if self.bitno == 8:
|
||||
self.bitno = 0
|
||||
self.index += 1
|
||||
self.add_bit(bool(pixel) != self.invert_alpha)
|
||||
|
||||
def end_row(self):
|
||||
"""
|
||||
Pad rows to a byte boundary
|
||||
"""
|
||||
if self.bitno != 0:
|
||||
self.bitno = 0
|
||||
self.index += 1
|
||||
|
||||
# The eight colours one bit per channel can express, for dithering.
|
||||
_RGB111_PALETTE = Image.new("P", (1, 1))
|
||||
_RGB111_PALETTE.putpalette([255 * int(x) for i in range(8) for x in f"{i:03b}"])
|
||||
|
||||
|
||||
class ImageRGB111(BitPackedEncoder):
|
||||
allow_config = {CONF_OPAQUE, CONF_ALPHA_CHANNEL}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: int,
|
||||
height: int,
|
||||
transparency: str,
|
||||
dither: Image.Dither,
|
||||
invert_alpha: bool,
|
||||
):
|
||||
bpp = 4 if transparency == CONF_ALPHA_CHANNEL else 3
|
||||
super().__init__(bpp, width, height, transparency, dither, invert_alpha)
|
||||
|
||||
def convert(self, image: Image, path: str):
|
||||
image = image.convert("RGBA")
|
||||
if self.dither != Image.Dither.FLOYDSTEINBERG:
|
||||
return image
|
||||
alpha = image.getchannel("A")
|
||||
image = image.convert("RGB").quantize(
|
||||
palette=_RGB111_PALETTE, dither=Image.Dither.FLOYDSTEINBERG
|
||||
)
|
||||
image = image.convert("RGB")
|
||||
image.putalpha(alpha)
|
||||
return image
|
||||
|
||||
def encode(self, pixel: tuple[int, int, int, int]):
|
||||
r, g, b, a = pixel
|
||||
if self.invert_alpha:
|
||||
a ^= 0xFF
|
||||
for channel in (r, g, b, a)[: self.bpp]:
|
||||
self.add_bit(channel > 127)
|
||||
|
||||
|
||||
class ImageGrayscale(ImageEncoder):
|
||||
@@ -315,6 +368,7 @@ class ReplaceWith:
|
||||
IMAGE_TYPE = {
|
||||
"BINARY": ImageBinary,
|
||||
"GRAYSCALE": ImageGrayscale,
|
||||
"RGB111": ImageRGB111,
|
||||
"RGB565": ImageRGB565,
|
||||
"RGB": ImageRGB,
|
||||
"TRANSPARENT_BINARY": ReplaceWith("'type: BINARY' and 'transparency: chroma_key'"),
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "image");
|
||||
|
||||
namespace esphome::image {
|
||||
|
||||
void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
||||
@@ -36,6 +38,17 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IMAGE_TYPE_RGB111: {
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
auto color = this->get_rgb111_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
@@ -91,6 +104,8 @@ Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off
|
||||
if (this->get_binary_pixel_(x, y))
|
||||
return color_on;
|
||||
return color_off;
|
||||
case IMAGE_TYPE_RGB111:
|
||||
return this->get_rgb111_pixel_(x, y);
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
return this->get_grayscale_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGB565:
|
||||
@@ -120,6 +135,10 @@ lv_image_dsc_t *Image::get_lv_image_dsc() {
|
||||
this->dsc_.header.cf = LV_COLOR_FORMAT_A8;
|
||||
break;
|
||||
|
||||
case IMAGE_TYPE_RGB111:
|
||||
// Rejected by the lvgl config validation
|
||||
ESP_LOGE(TAG, "RGB111 images are not supported in LVGL.");
|
||||
break;
|
||||
case IMAGE_TYPE_RGB:
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
@@ -152,6 +171,19 @@ bool Image::get_binary_pixel_(int x, int y) const {
|
||||
const uint32_t pos = x + y * width_8;
|
||||
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
||||
}
|
||||
Color Image::get_rgb111_pixel_(int x, int y) const {
|
||||
const uint32_t bitpos = y * this->get_width_stride() * 8 + x * this->bpp_;
|
||||
const uint8_t *byte = this->data_start_ + bitpos / 8;
|
||||
const uint8_t shift = bitpos % 8;
|
||||
// A pixel can straddle two bytes; read the second one only when it does.
|
||||
uint16_t window = progmem_read_byte(byte) << 8;
|
||||
if (shift + this->bpp_ > 8)
|
||||
window |= progmem_read_byte(byte + 1);
|
||||
const uint8_t bits = (window >> (16 - this->bpp_ - shift)) & ((1u << this->bpp_) - 1);
|
||||
const uint8_t rgb = this->bpp_ == 4 ? bits >> 1 : bits;
|
||||
const bool opaque = this->bpp_ == 3 || (bits & 1);
|
||||
return Color(rgb & 4 ? 0xFF : 0x00, rgb & 2 ? 0xFF : 0x00, rgb & 1 ? 0xFF : 0x00, opaque ? 0xFF : 0x00);
|
||||
}
|
||||
Color Image::get_rgb_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
|
||||
Color color = Color(progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 1),
|
||||
@@ -216,6 +248,9 @@ Image::Image(const uint8_t *data_start, int width, int height, ImageType type, T
|
||||
case IMAGE_TYPE_BINARY:
|
||||
this->bpp_ = 1;
|
||||
break;
|
||||
case IMAGE_TYPE_RGB111:
|
||||
this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 4 : 3;
|
||||
break;
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
this->bpp_ = 8;
|
||||
break;
|
||||
|
||||
@@ -13,6 +13,7 @@ enum ImageType {
|
||||
IMAGE_TYPE_GRAYSCALE = 1,
|
||||
IMAGE_TYPE_RGB = 2,
|
||||
IMAGE_TYPE_RGB565 = 3,
|
||||
IMAGE_TYPE_RGB111 = 4, // one bit per channel plus an optional alpha bit, for colour e-paper
|
||||
};
|
||||
|
||||
enum Transparency {
|
||||
@@ -44,6 +45,7 @@ class Image : public display::BaseImage {
|
||||
#endif
|
||||
protected:
|
||||
bool get_binary_pixel_(int x, int y) const;
|
||||
Color get_rgb111_pixel_(int x, int y) const;
|
||||
Color get_rgb_pixel_(int x, int y) const;
|
||||
Color get_rgb565_pixel_(int x, int y) const;
|
||||
Color get_grayscale_pixel_(int x, int y) const;
|
||||
|
||||
@@ -37,7 +37,6 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["split_buffer"]
|
||||
@@ -348,11 +347,8 @@ def _final_validate(config: ConfigType) -> None:
|
||||
config
|
||||
)
|
||||
|
||||
global_config = full_config.get()
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if display.has_external_writer(config[CONF_ID]):
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
else:
|
||||
|
||||
@@ -891,12 +891,10 @@ static uint8_t color_to_nibble(const Color &color) {
|
||||
if (color.raw_32 == 0xFFFFFFFF)
|
||||
return 0x0F; // white
|
||||
|
||||
// Derive luma from RGB using Rec.601 weights (0.299/0.587/0.114, scaled by
|
||||
// 256). Rec.601 is the standard for converting SDR images to grayscale and
|
||||
// spreads saturated colours across the mid-range; Rec.709 instead crams them
|
||||
// against white/black where the 16 panel levels are hard to tell apart.
|
||||
auto luma = static_cast<uint8_t>((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
|
||||
return quantize_8bit_to_nibble(luma);
|
||||
// Rec.601 luma is the standard for converting SDR images to grayscale and spreads saturated colours
|
||||
// across the mid-range; Rec.709 instead crams them against white/black where the 16 panel levels are
|
||||
// hard to tell apart.
|
||||
return quantize_8bit_to_nibble(display::rec601_luma(color));
|
||||
}
|
||||
|
||||
// 4x4 ordered (Bayer) dither threshold over the weighted-luma range (0..65535).
|
||||
|
||||
@@ -204,6 +204,7 @@ class IT8951Display : public Display,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
|
||||
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & TRANSFORM_SWAP_XY; }
|
||||
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; }
|
||||
|
||||
@@ -25,6 +25,7 @@ from esphome.components.image import (
|
||||
ImageBinary,
|
||||
ImageGrayscale,
|
||||
ImageRGB,
|
||||
ImageRGB111,
|
||||
ImageRGB565,
|
||||
get_image_metadata,
|
||||
)
|
||||
@@ -43,6 +44,7 @@ from esphome.const import (
|
||||
CONF_ROTATION,
|
||||
CONF_TIMEOUT,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from esphome.core import CORE, ID, Lambda
|
||||
from esphome.cpp_generator import MockObj
|
||||
@@ -293,6 +295,21 @@ def final_validation(config_list):
|
||||
if len(config_list) != 1:
|
||||
multi_conf_validate(config_list)
|
||||
|
||||
for img in get_lv_images_used():
|
||||
path = global_config.get_path_for_id(img.id)[:-1]
|
||||
# An image nested in another platform (sendspin's current_image) takes its type from the parent
|
||||
while path:
|
||||
image_conf = global_config.get_config_for_path(path)
|
||||
if isinstance(image_conf, dict) and CONF_TYPE in image_conf:
|
||||
break
|
||||
path = path[:-1]
|
||||
else:
|
||||
continue
|
||||
if IMAGE_TYPE.get(image_conf[CONF_TYPE]) is ImageRGB111:
|
||||
raise cv.FinalExternalInvalid(
|
||||
f"Image '{img}': LVGL does not support RGB111 images, use RGB565 or RGB instead",
|
||||
path + [CONF_TYPE],
|
||||
)
|
||||
for w in get_focused_widgets():
|
||||
path = global_config.get_path_for_id(w)
|
||||
widget_conf = global_config.get_config_for_path(path[:-1])
|
||||
|
||||
@@ -85,7 +85,7 @@ class LvColorPickerType : public LvCompound {
|
||||
if (left != nullptr) {
|
||||
lv_obj_set_style_align(left, LV_ALIGN_LEFT_MID, LV_PART_MAIN);
|
||||
// A narrow hue column may be slimmer than its label.
|
||||
lv_obj_add_flag(left, LV_OBJ_FLAG_OVERFLOW_VISIBLE);
|
||||
lv_obj_set_flag(left, LV_OBJ_FLAG_OVERFLOW_VISIBLE, true);
|
||||
}
|
||||
|
||||
// The text rows keep their natural height and the bars between them take what is left,
|
||||
@@ -233,7 +233,7 @@ class LvColorPickerType : public LvCompound {
|
||||
static lv_obj_t *create_box(lv_obj_t *parent) {
|
||||
auto *obj = lv_obj_create(parent);
|
||||
lv_obj_add_style(obj, &plain_box, LV_PART_MAIN);
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_flag(obj, LV_OBJ_FLAG_SCROLLABLE, false);
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ class LvColorPickerType : public LvCompound {
|
||||
lv_obj_set_style_radius(disc, LV_RADIUS_CIRCLE, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(disc, LV_OPA_COVER, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_grad(disc, grad, LV_PART_MAIN);
|
||||
lv_obj_remove_flag(disc, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_flag(disc, LV_OBJ_FLAG_SCROLLABLE, false);
|
||||
return disc;
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ class LvColorPickerType : public LvCompound {
|
||||
this->wheel_ = create_disc(container, &wheel_hue_grad);
|
||||
// Not clickable, so touches fall through to the wheel underneath.
|
||||
auto *overlay = create_disc(this->wheel_, &wheel_white_grad);
|
||||
lv_obj_remove_flag(overlay, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_flag(overlay, LV_OBJ_FLAG_CLICKABLE, false);
|
||||
|
||||
this->wheel_handle_ = lv_obj_create(this->wheel_);
|
||||
lv_obj_set_size(this->wheel_handle_, WHEEL_HANDLE_SIZE, WHEEL_HANDLE_SIZE);
|
||||
@@ -345,8 +345,8 @@ class LvColorPickerType : public LvCompound {
|
||||
lv_obj_set_style_bg_opa(this->wheel_handle_, LV_OPA_TRANSP, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(this->wheel_handle_, 2, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_color(this->wheel_handle_, lv_color_white(), LV_PART_MAIN);
|
||||
lv_obj_remove_flag(this->wheel_handle_, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(this->wheel_handle_, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_flag(this->wheel_handle_, LV_OBJ_FLAG_CLICKABLE, false);
|
||||
lv_obj_set_flag(this->wheel_handle_, LV_OBJ_FLAG_SCROLLABLE, false);
|
||||
|
||||
// PRESSED as well, so a tap takes effect without the finger moving.
|
||||
lv_obj_add_event_cb(this->wheel_, wheel_touch_cb, LV_EVENT_PRESSED, this);
|
||||
@@ -359,7 +359,7 @@ class LvColorPickerType : public LvCompound {
|
||||
|
||||
void add_value_label_(SliderIndex index, lv_color_t color) {
|
||||
auto *slider = this->sliders_[index];
|
||||
lv_obj_add_flag(slider, LV_OBJ_FLAG_OVERFLOW_VISIBLE);
|
||||
lv_obj_set_flag(slider, LV_OBJ_FLAG_OVERFLOW_VISIBLE, true);
|
||||
auto *label = lv_label_create(slider);
|
||||
this->values_[index] = label;
|
||||
lv_obj_set_style_align(label, LV_ALIGN_CENTER, LV_PART_MAIN);
|
||||
@@ -509,7 +509,7 @@ class LvColorPickerType : public LvCompound {
|
||||
static void bubble_events(lv_obj_t *obj) {
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_count(obj); i++) {
|
||||
auto *child = lv_obj_get_child(obj, i);
|
||||
lv_obj_add_flag(child, LV_OBJ_FLAG_EVENT_BUBBLE);
|
||||
lv_obj_set_flag(child, LV_OBJ_FLAG_EVENT_BUBBLE, true);
|
||||
bubble_events(child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ enum class Orientation : uint8_t {
|
||||
};
|
||||
|
||||
class LvglComponent final : public PollingComponent {
|
||||
constexpr static const char *const TAG = "lvgl";
|
||||
constexpr static const char *const TAG = "lvgl"; // NOLINT: lvgl does not build for ESP8266
|
||||
|
||||
public:
|
||||
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
|
||||
|
||||
@@ -19,7 +19,7 @@ using alloc_string_t = std::basic_string<char, std::char_traits<char>, RAMAlloca
|
||||
* @tparam V The type of the value in the mapping. Should be a basic type or pointer.
|
||||
*/
|
||||
|
||||
static const char *const TAG = "mapping";
|
||||
ESPHOME_LOG_TAG(TAG, "mapping");
|
||||
|
||||
template<typename K, typename V> class Mapping {
|
||||
public:
|
||||
|
||||
@@ -3,7 +3,9 @@ from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
|
||||
from esphome.config_helpers import filter_source_files_from_platform, get_logger_level
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_API,
|
||||
CONF_DISABLED,
|
||||
CONF_ETHERNET,
|
||||
CONF_ID,
|
||||
CONF_OPENTHREAD,
|
||||
CONF_PORT,
|
||||
@@ -285,6 +287,23 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# With both wifi and ethernet, the _esphomelib "network" TXT value follows the
|
||||
# connected interface. OpenThread hands its services to the SRP client once,
|
||||
# so it keeps the fixed value.
|
||||
if (
|
||||
CONF_API in CORE.config
|
||||
and CONF_WIFI in CORE.config
|
||||
and CONF_ETHERNET in CORE.config
|
||||
and CONF_OPENTHREAD not in CORE.config
|
||||
):
|
||||
from esphome.components import network
|
||||
|
||||
cg.add_define("USE_MDNS_DYNAMIC_NETWORK_TXT")
|
||||
# Where the network component tracks the default route (ESP32), follow
|
||||
# it; otherwise mdns checks the connected interface every second.
|
||||
if await network.register_active_interface_listener(var):
|
||||
cg.add_define("USE_MDNS_ACTIVE_INTERFACE_LISTENER")
|
||||
|
||||
for service in config[CONF_SERVICES]:
|
||||
txt_records = [
|
||||
await _mdns_txt_record_templated(var, txt_key, txt_value)
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
#ifdef USE_DASHBOARD_IMPORT
|
||||
#include "esphome/components/dashboard_import/dashboard_import.h"
|
||||
#endif
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
#include "esphome/components/ethernet/ethernet_component.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::mdns {
|
||||
|
||||
@@ -39,6 +43,57 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
|
||||
// Wrap build-time defines into flash storage
|
||||
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
|
||||
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
|
||||
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
|
||||
|
||||
// Same order as the default route in NetworkComponent::loop()
|
||||
const char *MDNSComponent::connected_network_() const {
|
||||
const bool ethernet = ethernet::global_eth_component->is_connected();
|
||||
const bool wifi = wifi::global_wifi_component->is_connected();
|
||||
#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
|
||||
if (wifi)
|
||||
return NETWORK_WIFI;
|
||||
#endif
|
||||
if (ethernet)
|
||||
return NETWORK_ETHERNET;
|
||||
if (wifi)
|
||||
return NETWORK_WIFI;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void MDNSComponent::update_network_txt_(const char *network) {
|
||||
// With no interface connected, keep the last value instead of flapping to wifi
|
||||
if (this->network_txt_ == nullptr || network == nullptr || network == this->network_txt_)
|
||||
return;
|
||||
if (!this->publish_network_txt_(network)) {
|
||||
// The _esphomelib service is missing, so a retry would fail too
|
||||
this->network_txt_ = nullptr;
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Network TXT: %s", network);
|
||||
this->network_txt_ = network;
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
this->network_record_->value = MDNS_STR(network);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
|
||||
void MDNSComponent::on_active_interface(network::NetworkInterface interface) {
|
||||
switch (interface) {
|
||||
case network::NetworkInterface::NETWORK_INTERFACE_ETHERNET:
|
||||
this->update_network_txt_(NETWORK_ETHERNET);
|
||||
break;
|
||||
case network::NetworkInterface::NETWORK_INTERFACE_WIFI:
|
||||
this->update_network_txt_(NETWORK_WIFI);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_register) {
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
auto &services = this->services_;
|
||||
@@ -160,7 +215,15 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
||||
|
||||
txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
|
||||
|
||||
#if defined(USE_WIFI)
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
const char *network = this->connected_network_();
|
||||
this->network_txt_ = network != nullptr ? network : NETWORK_WIFI;
|
||||
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(this->network_txt_)});
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
// services is services_ here, which never reallocates
|
||||
this->network_record_ = &txt_records.back();
|
||||
#endif
|
||||
#elif defined(USE_WIFI)
|
||||
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
|
||||
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
|
||||
#elif defined(USE_ETHERNET)
|
||||
|
||||
@@ -32,6 +32,13 @@
|
||||
#define USE_MDNS_DEVICE_INFO_TXT
|
||||
#endif
|
||||
|
||||
// USE_MDNS_DYNAMIC_NETWORK_TXT (wifi + ethernet + api): the _esphomelib "network" TXT value follows the
|
||||
// connected interface. With USE_MDNS_ACTIVE_INTERFACE_LISTENER (ESP32) the network component reports
|
||||
// default route changes; otherwise the value is checked every NETWORK_TXT_CHECK_INTERVAL_MS.
|
||||
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
|
||||
#include "esphome/components/network/network_component.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::mdns {
|
||||
|
||||
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
|
||||
@@ -73,6 +80,10 @@ class MDNSComponent final : public Component
|
||||
,
|
||||
public ethernet::EthernetIPStateListener
|
||||
#endif
|
||||
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
|
||||
,
|
||||
public network::ActiveInterfaceListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -134,6 +145,10 @@ class MDNSComponent final : public Component
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
|
||||
void on_active_interface(network::NetworkInterface interface) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
|
||||
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
|
||||
const network::IPAddress &dns2) override;
|
||||
@@ -150,6 +165,22 @@ class MDNSComponent final : public Component
|
||||
|
||||
void setup_buffers_and_register_(PlatformRegisterFn platform_register);
|
||||
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
static constexpr uint32_t NETWORK_TXT_CHECK_INTERVAL_MS = 1000;
|
||||
/// The first connected interface in network: priority order ("ethernet" or "wifi"), else nullptr.
|
||||
const char *connected_network_() const;
|
||||
/// Publish network as the "network" TXT value if it changed; nullptr keeps the current value.
|
||||
void update_network_txt_(const char *network);
|
||||
/// Platform-specific: replace the "network" TXT value of the registered _esphomelib service.
|
||||
bool publish_network_txt_(const char *network);
|
||||
/// Advertised value; nullptr until the _esphomelib service is registered, and after a failed publish.
|
||||
const char *network_txt_{nullptr};
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
/// The "network" record in services_, kept in step with network_txt_.
|
||||
MDNSTXTRecord *network_record_{nullptr};
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_MDNS_DYNAMIC_TXT
|
||||
/// Storage for runtime-generated TXT values from user lambdas
|
||||
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
|
||||
|
||||
@@ -89,7 +89,25 @@ bool MDNSComponent::set_service_enabled(const char *service_type, const char *pr
|
||||
}
|
||||
#endif // USE_MDNS_SUPPORTS_ENABLE_DISABLE && !USE_OPENTHREAD
|
||||
|
||||
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); }
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
bool MDNSComponent::publish_network_txt_(const char *network) {
|
||||
// ESP-IDF copies the value and announces the changed record
|
||||
esp_err_t err = mdns_service_txt_item_set("_esphomelib", "_tcp", "network", network);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to update network TXT: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
this->setup_buffers_and_register_(register_esp32);
|
||||
#if defined(USE_MDNS_DYNAMIC_NETWORK_TXT) && !defined(USE_MDNS_ACTIVE_INTERFACE_LISTENER)
|
||||
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS,
|
||||
[this]() { this->update_network_txt_(this->connected_network_()); });
|
||||
#endif
|
||||
}
|
||||
|
||||
void MDNSComponent::on_shutdown() {
|
||||
#ifndef USE_OPENTHREAD
|
||||
|
||||
@@ -46,7 +46,27 @@ void MDNSComponent::start_polling_window_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
ESPHOME_LOG_TAG(TAG, "mdns");
|
||||
|
||||
bool MDNSComponent::publish_network_txt_(const char *network) {
|
||||
// LEAmDNS replaces the value of an existing key; announce() sends the change
|
||||
if (!MDNS.addServiceTxt("esphomelib", "tcp", "network", network)) {
|
||||
ESP_LOGW(TAG, "Failed to update network TXT");
|
||||
return false;
|
||||
}
|
||||
MDNS.announce();
|
||||
this->start_polling_window_();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
// No-op until the first IP-up event registers the services
|
||||
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS,
|
||||
[this]() { this->update_network_txt_(this->connected_network_()); });
|
||||
#endif
|
||||
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
|
||||
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
|
||||
// own IP state listener. Both WiFi and Ethernet have the same listener signature —
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace esphome::midea::ac {
|
||||
|
||||
const char *const Constants::TAG = "midea";
|
||||
const char Constants::TAG[] PROGMEM = "midea";
|
||||
const char *const Constants::FREEZE_PROTECTION = "Freeze Protection";
|
||||
const char *const Constants::SILENT = "Silent";
|
||||
const char *const Constants::TURBO = "Turbo";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user