mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 12:12:48 +00:00
Merge remote-tracking branch 'origin/dev' into climate-custom-modes-const-vector
# Conflicts: # esphome/components/midea/air_conditioner.cpp
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"]
|
||||
@@ -49,6 +54,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 +120,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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -62,12 +62,9 @@ const char *MDNSComponent::connected_network_() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void MDNSComponent::update_network_txt_() {
|
||||
if (this->network_txt_ == nullptr)
|
||||
return;
|
||||
void MDNSComponent::update_network_txt_(const char *network) {
|
||||
// With no interface connected, keep the last value instead of flapping to wifi
|
||||
const char *network = this->connected_network_();
|
||||
if (network == nullptr || network == this->network_txt_)
|
||||
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
|
||||
@@ -80,6 +77,21 @@ void MDNSComponent::update_network_txt_() {
|
||||
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) {
|
||||
|
||||
@@ -32,10 +32,11 @@
|
||||
#define USE_MDNS_DEVICE_INFO_TXT
|
||||
#endif
|
||||
|
||||
// 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 defined(USE_API) && defined(USE_WIFI) && defined(USE_ETHERNET) && !defined(USE_OPENTHREAD)
|
||||
#define USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
// 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 {
|
||||
@@ -79,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;
|
||||
@@ -140,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;
|
||||
@@ -160,8 +169,8 @@ class MDNSComponent final : public Component
|
||||
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;
|
||||
/// Check the connected interface and publish a changed "network" TXT value.
|
||||
void update_network_txt_();
|
||||
/// 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.
|
||||
|
||||
@@ -103,8 +103,9 @@ bool MDNSComponent::publish_network_txt_(const char *network) {
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
this->setup_buffers_and_register_(register_esp32);
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS, [this]() { this->update_network_txt_(); });
|
||||
#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
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ bool MDNSComponent::publish_network_txt_(const char *network) {
|
||||
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->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
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
|
||||
namespace esphome::midea::ac {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "midea");
|
||||
|
||||
static void set_sensor(Sensor *sensor, float value) {
|
||||
if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
|
||||
sensor->publish_state(value);
|
||||
@@ -129,23 +127,40 @@ ClimateTraits AirConditioner::traits() {
|
||||
return traits;
|
||||
}
|
||||
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
static const LogString *display_control_str(DisplayControl control) {
|
||||
switch (control) {
|
||||
case DisplayControl::DISPLAY_CONTROL_UART:
|
||||
return LOG_STR("UART");
|
||||
case DisplayControl::DISPLAY_CONTROL_IR:
|
||||
return LOG_STR("IR");
|
||||
default:
|
||||
return LOG_STR("AUTO");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void AirConditioner::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
ESP_LOGCONFIG(Constants::TAG,
|
||||
"MideaDongle:\n"
|
||||
" [x] Period: %" PRIu32 "ms\n"
|
||||
" [x] Response timeout: %" PRIu32 "ms\n"
|
||||
" [x] Request attempts: %d",
|
||||
this->base_.getPeriod(), this->base_.getTimeout(), this->base_.getNumAttempts());
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
ESP_LOGCONFIG(TAG, " [x] Using RemoteTransmitter");
|
||||
ESP_LOGCONFIG(Constants::TAG,
|
||||
" [x] Using RemoteTransmitter\n"
|
||||
" [x] Display control: %s",
|
||||
LOG_STR_ARG(display_control_str(this->display_control_)));
|
||||
#endif
|
||||
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
|
||||
this->base_.getCapabilities().dump();
|
||||
} else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) {
|
||||
ESP_LOGW(TAG, "Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your "
|
||||
"appliance options.");
|
||||
ESP_LOGW(Constants::TAG,
|
||||
"Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your "
|
||||
"appliance options.");
|
||||
}
|
||||
this->dump_traits_(TAG);
|
||||
this->dump_traits_(Constants::TAG);
|
||||
}
|
||||
|
||||
/* ACTIONS */
|
||||
@@ -154,7 +169,7 @@ void AirConditioner::do_follow_me(float temperature, bool use_fahrenheit, bool b
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
// Check if temperature is finite (not NaN or infinite)
|
||||
if (!std::isfinite(temperature)) {
|
||||
ESP_LOGW(TAG, "Follow me action requires a finite temperature, got: %f", temperature);
|
||||
ESP_LOGW(Constants::TAG, "Follow me action requires a finite temperature, got: %f", temperature);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -163,14 +178,14 @@ void AirConditioner::do_follow_me(float temperature, bool use_fahrenheit, bool b
|
||||
static_cast<uint8_t>(esphome::clamp<long>(std::lroundf(temperature), 0L, static_cast<long>(UINT8_MAX)));
|
||||
|
||||
char temp_symbol = use_fahrenheit ? 'F' : 'C';
|
||||
ESP_LOGD(TAG, "Follow me action called with temperature: %.5f °%c, rounded to: %u °%c", temperature, temp_symbol,
|
||||
temp_uint8, temp_symbol);
|
||||
ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %.5f °%c, rounded to: %u °%c", temperature,
|
||||
temp_symbol, temp_uint8, temp_symbol);
|
||||
|
||||
// Create and transmit the data
|
||||
IrFollowMeData data(temp_uint8, use_fahrenheit, beeper);
|
||||
this->transmitter_.transmit(data);
|
||||
#else
|
||||
ESP_LOGW(TAG, "Action needs remote_transmitter component");
|
||||
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -179,21 +194,25 @@ void AirConditioner::do_swing_step() {
|
||||
IrSpecialData data(0x01);
|
||||
this->transmitter_.transmit(data);
|
||||
#else
|
||||
ESP_LOGW(TAG, "Action needs remote_transmitter component");
|
||||
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
|
||||
#endif
|
||||
}
|
||||
|
||||
void AirConditioner::do_display_toggle() {
|
||||
if (this->base_.getCapabilities().supportLightControl()) {
|
||||
this->base_.displayToggle();
|
||||
} else {
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
// AUTO uses IR unless the appliance reports light control over UART. Without a transmitter
|
||||
// built in, every setting sends the UART request: Mundoclima, Rotenso and others accept it
|
||||
// without advertising it in the 0xB5 capabilities report.
|
||||
const bool use_ir = this->display_control_ == DisplayControl::DISPLAY_CONTROL_IR ||
|
||||
(this->display_control_ == DisplayControl::DISPLAY_CONTROL_AUTO &&
|
||||
!this->base_.getCapabilities().supportLightControl());
|
||||
if (use_ir) {
|
||||
IrSpecialData data(0x08);
|
||||
this->transmitter_.transmit(data);
|
||||
#else
|
||||
ESP_LOGW(TAG, "Action needs remote_transmitter component");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->base_.displayToggle();
|
||||
}
|
||||
|
||||
} // namespace esphome::midea::ac
|
||||
|
||||
@@ -21,12 +21,20 @@ using climate::ClimateModeMask;
|
||||
using climate::ClimateSwingModeMask;
|
||||
using climate::ClimatePresetMask;
|
||||
|
||||
// How the display toggle reaches the appliance; the AUTO rules are in do_display_toggle()
|
||||
enum class DisplayControl : uint8_t {
|
||||
DISPLAY_CONTROL_UART, // always the UART request
|
||||
DISPLAY_CONTROL_AUTO, // UART when the appliance reports it or no transmitter is built in, else IR
|
||||
DISPLAY_CONTROL_IR, // always the IR remote, needs a transmitter
|
||||
};
|
||||
|
||||
class AirConditioner final : public ApplianceBase<dudanov::midea::ac::AirConditioner>, public climate::Climate {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; }
|
||||
void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; }
|
||||
void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; }
|
||||
void set_display_control(DisplayControl control) { this->display_control_ = control; }
|
||||
void on_status_change() override;
|
||||
|
||||
/* ############### */
|
||||
@@ -52,6 +60,7 @@ class AirConditioner final : public ApplianceBase<dudanov::midea::ac::AirConditi
|
||||
ClimateSwingModeMask supported_swing_modes_{};
|
||||
ClimatePresetMask supported_presets_{};
|
||||
bool frost_protection_set_{false};
|
||||
DisplayControl display_control_{DisplayControl::DISPLAY_CONTROL_AUTO};
|
||||
Sensor *outdoor_sensor_{nullptr};
|
||||
Sensor *humidity_sensor_{nullptr};
|
||||
Sensor *power_sensor_{nullptr};
|
||||
|
||||
@@ -33,15 +33,18 @@ from esphome.const import (
|
||||
UNIT_WATT,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@dudanov"]
|
||||
DEPENDENCIES = ["climate", "uart"]
|
||||
AUTO_LOAD = ["sensor"]
|
||||
CONF_POWER_USAGE = "power_usage"
|
||||
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
|
||||
CONF_DISPLAY_CONTROL = "display_control"
|
||||
midea_ac_ns = cg.esphome_ns.namespace("midea").namespace("ac")
|
||||
AirConditioner = midea_ac_ns.class_("AirConditioner", climate.Climate, cg.Component)
|
||||
Capabilities = midea_ac_ns.namespace("Constants")
|
||||
DisplayControl = midea_ac_ns.enum("DisplayControl", is_class=True)
|
||||
|
||||
|
||||
def templatize(value):
|
||||
@@ -82,11 +85,29 @@ CUSTOM_PRESETS = {
|
||||
"FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION,
|
||||
}
|
||||
|
||||
# How midea_ac.display_toggle reaches the appliance; see do_display_toggle() for the AUTO rules
|
||||
DISPLAY_CONTROLS = {
|
||||
"UART": DisplayControl.DISPLAY_CONTROL_UART,
|
||||
"AUTO": DisplayControl.DISPLAY_CONTROL_AUTO,
|
||||
"IR": DisplayControl.DISPLAY_CONTROL_IR,
|
||||
}
|
||||
|
||||
validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True)
|
||||
validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True)
|
||||
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
|
||||
validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True)
|
||||
validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True)
|
||||
validate_display_control = cv.enum(DISPLAY_CONTROLS, upper=True)
|
||||
|
||||
|
||||
def validate_display_control_with_transmitter(config: ConfigType) -> ConfigType:
|
||||
"""`display_control: IR` needs a `remote_transmitter` to send through; UART and AUTO do not."""
|
||||
if config.get(CONF_DISPLAY_CONTROL) == "IR" and CONF_TRANSMITTER_ID not in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_DISPLAY_CONTROL}: IR' requires '{CONF_TRANSMITTER_ID}' to be set"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
climate.climate_schema(AirConditioner)
|
||||
@@ -100,6 +121,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_AUTOCONF, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISPLAY_CONTROL, default="AUTO"): validate_display_control,
|
||||
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes),
|
||||
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
|
||||
validate_swing_modes
|
||||
@@ -134,6 +156,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
validate_display_control_with_transmitter,
|
||||
cv.only_on(
|
||||
[
|
||||
PLATFORM_ESP32,
|
||||
@@ -204,6 +227,7 @@ async def to_code(config):
|
||||
cg.add(var.set_transmitter(transmitter_))
|
||||
cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
|
||||
cg.add(var.set_autoconf(config[CONF_AUTOCONF]))
|
||||
cg.add(var.set_display_control(config[CONF_DISPLAY_CONTROL]))
|
||||
if CONF_SUPPORTED_MODES in config:
|
||||
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
|
||||
if CONF_SUPPORTED_SWING_MODES in config:
|
||||
|
||||
@@ -100,6 +100,37 @@ if (
|
||||
|
||||
network_ns = cg.esphome_ns.namespace("network")
|
||||
NetworkComponent = network_ns.class_("NetworkComponent", cg.Component)
|
||||
|
||||
_request_active_interface_listener_slot = cg.slot_counter(
|
||||
"NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT"
|
||||
)
|
||||
|
||||
|
||||
def has_active_interface() -> bool:
|
||||
"""Return True when NetworkComponent tracks the interface holding the default route.
|
||||
|
||||
That is ESP32 with more than one interface in 'network: priority:', the same
|
||||
condition that emits USE_NETWORK_DEFAULT_ROUTE.
|
||||
"""
|
||||
return CORE.is_esp32 and len(CORE.config.get(DOMAIN, {}).get(CONF_PRIORITY, [])) > 1
|
||||
|
||||
|
||||
async def register_active_interface_listener(listener: cg.MockObj) -> bool:
|
||||
"""Register an ActiveInterfaceListener with the NetworkComponent.
|
||||
|
||||
The listener is notified from the main loop when the interface holding the
|
||||
default route changes. Returns False, and registers nothing, when
|
||||
has_active_interface() is False; the caller then needs another way to
|
||||
follow interface changes.
|
||||
"""
|
||||
if not has_active_interface():
|
||||
return False
|
||||
_request_active_interface_listener_slot()
|
||||
network = await cg.get_variable(CORE.config[DOMAIN][CONF_ID])
|
||||
cg.add(network.add_active_interface_listener(listener))
|
||||
return True
|
||||
|
||||
|
||||
IPAddress = network_ns.class_("IPAddress")
|
||||
|
||||
|
||||
@@ -402,7 +433,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# frameworks build from source.
|
||||
# The ethernet/wifi-only assumption behind the arbitration is enforced in
|
||||
# _final_validate() so a future unsupported type fails as a config error.
|
||||
if len(priority_list) > 1 and CORE.is_esp32:
|
||||
if has_active_interface():
|
||||
cg.add_define("USE_NETWORK_DEFAULT_ROUTE")
|
||||
# Have lwIP switch to the DNS servers of the netif that owns the
|
||||
# default route whenever the arbitration changes it.
|
||||
|
||||
@@ -78,16 +78,24 @@ void NetworkComponent::loop() {
|
||||
// USE_NETWORK_PRIMARY_INTERFACE_WIFI is emitted for a wifi-first priority list;
|
||||
// it selects the reported address in util.cpp and doubles as the route-order
|
||||
// pivot here — the two uses must stay in sync.
|
||||
esp_netif_t *best;
|
||||
esp_netif_t *wifi = connected_wifi_netif();
|
||||
esp_netif_t *ethernet = connected_ethernet_netif();
|
||||
#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
|
||||
best = connected_wifi_netif();
|
||||
if (best == nullptr)
|
||||
best = connected_ethernet_netif();
|
||||
esp_netif_t *best = wifi != nullptr ? wifi : ethernet;
|
||||
#else
|
||||
best = connected_ethernet_netif();
|
||||
if (best == nullptr)
|
||||
best = connected_wifi_netif();
|
||||
esp_netif_t *best = ethernet != nullptr ? ethernet : wifi;
|
||||
#endif
|
||||
NetworkInterface active = NetworkInterface::NETWORK_INTERFACE_NONE;
|
||||
if (best != nullptr) {
|
||||
active = best == ethernet ? NetworkInterface::NETWORK_INTERFACE_ETHERNET : NetworkInterface::NETWORK_INTERFACE_WIFI;
|
||||
}
|
||||
if (active != this->active_interface_) {
|
||||
this->active_interface_ = active;
|
||||
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
|
||||
for (auto *listener : this->active_interface_listeners_)
|
||||
listener->on_active_interface(active);
|
||||
#endif
|
||||
}
|
||||
if (best == nullptr) {
|
||||
// Forget the last winner: stopping its netif cleared lwIP's default route and
|
||||
// IDF's manual override suppresses re-election, so reconnect must re-assert it.
|
||||
|
||||
@@ -4,12 +4,32 @@
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#ifdef USE_NETWORK_DEFAULT_ROUTE
|
||||
#include "esphome/core/helpers.h"
|
||||
// Forward declaration matching esp_netif's own typedef; avoids pulling esp_netif.h
|
||||
// into this header.
|
||||
using esp_netif_t = struct esp_netif_obj;
|
||||
#endif
|
||||
|
||||
namespace esphome::network {
|
||||
|
||||
#ifdef USE_NETWORK_DEFAULT_ROUTE
|
||||
/// The interface holding the default route. Lists only the interfaces loop() arbitrates
|
||||
/// (ARBITRATED_NETWORK_TYPES in __init__.py); extend both together.
|
||||
enum class NetworkInterface : uint8_t {
|
||||
NETWORK_INTERFACE_NONE,
|
||||
NETWORK_INTERFACE_ETHERNET,
|
||||
NETWORK_INTERFACE_WIFI,
|
||||
};
|
||||
|
||||
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
|
||||
/// Notified from the main loop when the interface holding the default route changes.
|
||||
class ActiveInterfaceListener {
|
||||
public:
|
||||
virtual void on_active_interface(NetworkInterface interface) = 0;
|
||||
};
|
||||
#endif
|
||||
#endif
|
||||
|
||||
class NetworkComponent final : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -20,6 +40,14 @@ class NetworkComponent final : public Component {
|
||||
#ifdef USE_NETWORK_DEFAULT_ROUTE
|
||||
void loop() override;
|
||||
|
||||
/// The first connected interface in network: priority order; NONE while none is connected.
|
||||
NetworkInterface get_active_interface() const { return this->active_interface_; }
|
||||
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
|
||||
void add_active_interface_listener(ActiveInterfaceListener *listener) {
|
||||
this->active_interface_listeners_.push_back(listener);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Verify-lwIP-route interval for the settled state; keeps the global lwIP core
|
||||
// mutex off the hot loop path.
|
||||
@@ -27,6 +55,10 @@ class NetworkComponent final : public Component {
|
||||
// Last netif this component made the default; avoids redundant esp_netif calls.
|
||||
esp_netif_t *default_netif_{nullptr};
|
||||
uint32_t last_route_check_{0};
|
||||
#ifdef NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT
|
||||
StaticVector<ActiveInterfaceListener *, NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT> active_interface_listeners_;
|
||||
#endif
|
||||
NetworkInterface active_interface_{NetworkInterface::NETWORK_INTERFACE_NONE};
|
||||
#endif
|
||||
};
|
||||
} // namespace esphome::network
|
||||
|
||||
@@ -14,6 +14,15 @@ static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts
|
||||
// Numeric timeout IDs. Each chain is sequential, so only one timeout per ID is active at a time.
|
||||
static constexpr uint32_t TIMEOUT_POLL = 1;
|
||||
static constexpr uint32_t TIMEOUT_SETUP_STEP = 2;
|
||||
static constexpr uint32_t TIMEOUT_STARTUP = 3;
|
||||
// Post-command wait times from the datasheet: start execution (4.8.1), stop execution (4.8.2),
|
||||
// fan cleaning (4.8.22), SHT heater (4.8.23), CO2 conditioning after start on SEN63C/SEN69C (4.8.1)
|
||||
static constexpr uint32_t START_MEASUREMENT_DELAY = 50;
|
||||
static constexpr uint32_t STOP_MEASUREMENT_DELAY = 1400;
|
||||
static constexpr uint32_t FAN_CLEANING_DELAY = 10000;
|
||||
static constexpr uint32_t SHT_HEATER_DELAY = 20000;
|
||||
static constexpr uint32_t CO2_CONDITIONING_DELAY = 24000;
|
||||
static constexpr uint32_t STARTUP_DELAY = 60000; // ms before published values are considered valid
|
||||
static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014;
|
||||
@@ -27,6 +36,9 @@ static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN68 = 0x0467;
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN69C = 0x04B5;
|
||||
|
||||
static constexpr uint16_t SEN6X_CMD_START_MEASUREMENTS = 0x0021;
|
||||
static constexpr uint16_t SEN6X_CMD_STOP_MEASUREMENTS = 0x0104;
|
||||
static constexpr uint16_t SEN6X_CMD_START_FAN_CLEANING = 0x5607;
|
||||
static constexpr uint16_t SEN6X_CMD_ACTIVATE_SHT_HEATER = 0x6765;
|
||||
static constexpr uint16_t SEN6X_CMD_RESET = 0xD304;
|
||||
static constexpr uint16_t SEN6X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
|
||||
static constexpr uint16_t SEN6X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
|
||||
@@ -191,7 +203,7 @@ void SEN6XComponent::finish_setup_() {
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(60000, [this]() { this->startup_complete_ = true; });
|
||||
this->on_measurement_started_();
|
||||
this->initialized_ = true;
|
||||
ESP_LOGD(TAG, "Initialized");
|
||||
}
|
||||
@@ -240,7 +252,7 @@ void SEN6XComponent::dump_config() {
|
||||
}
|
||||
|
||||
void SEN6XComponent::update() {
|
||||
if (!this->initialized_) {
|
||||
if (!this->initialized_ || !this->measuring_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -442,6 +454,101 @@ void SEN6XComponent::parse_and_publish_measurements_() {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
// True while the previous command's execution or settling time has not elapsed
|
||||
bool SEN6XComponent::command_blocked_() const { return millis() - this->command_started_at_ < this->command_wait_; }
|
||||
|
||||
void SEN6XComponent::set_command_wait_(uint32_t wait_ms) {
|
||||
this->command_started_at_ = millis();
|
||||
this->command_wait_ = wait_ms;
|
||||
}
|
||||
|
||||
void SEN6XComponent::on_measurement_started_() {
|
||||
this->measuring_ = true;
|
||||
this->set_command_wait_(START_MEASUREMENT_DELAY);
|
||||
// SEN63C/SEN69C condition the CO2 sensor for 24 s after a start; block restarts until then
|
||||
if (this->sen6x_type_ == SEN63C || this->sen6x_type_ == SEN69C) {
|
||||
this->co2_started_at_ = millis();
|
||||
this->co2_wait_ = CO2_CONDITIONING_DELAY;
|
||||
}
|
||||
// Values need the warm-up period again after every start
|
||||
this->startup_complete_ = false;
|
||||
this->set_timeout(TIMEOUT_STARTUP, STARTUP_DELAY, [this]() { this->startup_complete_ = true; });
|
||||
}
|
||||
|
||||
void SEN6XComponent::start_measurement() {
|
||||
if (!this->initialized_ || this->measuring_)
|
||||
return;
|
||||
// The CO2 conditioning window only blocks restarting, not other commands
|
||||
if (this->command_blocked_() || millis() - this->co2_started_at_ < this->co2_wait_) {
|
||||
ESP_LOGW(TAG, "Device busy");
|
||||
return;
|
||||
}
|
||||
if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "Start measurement failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
this->on_measurement_started_();
|
||||
}
|
||||
|
||||
void SEN6XComponent::stop_measurement() {
|
||||
if (!this->initialized_ || !this->measuring_)
|
||||
return;
|
||||
if (this->command_blocked_()) {
|
||||
ESP_LOGW(TAG, "Device busy");
|
||||
return;
|
||||
}
|
||||
// Stop any in-flight polling before the device goes idle
|
||||
this->cancel_timeout(TIMEOUT_POLL);
|
||||
if (!this->write_command(SEN6X_CMD_STOP_MEASUREMENTS)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "Stop measurement failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
this->measuring_ = false;
|
||||
this->set_command_wait_(STOP_MEASUREMENT_DELAY);
|
||||
}
|
||||
|
||||
void SEN6XComponent::start_fan_cleaning() {
|
||||
if (!this->initialized_)
|
||||
return;
|
||||
// The device only accepts this command in idle mode (datasheet section 4.8.22)
|
||||
if (this->measuring_) {
|
||||
ESP_LOGW(TAG, "Fan cleaning requires idle mode; stop the measurement first");
|
||||
return;
|
||||
}
|
||||
if (this->command_blocked_()) {
|
||||
ESP_LOGW(TAG, "Device busy");
|
||||
return;
|
||||
}
|
||||
if (!this->write_command(SEN6X_CMD_START_FAN_CLEANING)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "Fan cleaning failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
this->set_command_wait_(FAN_CLEANING_DELAY);
|
||||
}
|
||||
|
||||
void SEN6XComponent::activate_sht_heater() {
|
||||
if (!this->initialized_)
|
||||
return;
|
||||
// The device only accepts this command in idle mode (datasheet section 4.8.23)
|
||||
if (this->measuring_) {
|
||||
ESP_LOGW(TAG, "SHT heater requires idle mode; stop the measurement first");
|
||||
return;
|
||||
}
|
||||
if (this->command_blocked_()) {
|
||||
ESP_LOGW(TAG, "Device busy");
|
||||
return;
|
||||
}
|
||||
if (!this->write_command(SEN6X_CMD_ACTIVATE_SHT_HEATER)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "SHT heater failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
this->set_command_wait_(SHT_HEATER_DELAY);
|
||||
}
|
||||
|
||||
SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) {
|
||||
if (product_name == "SEN62")
|
||||
return SEN62;
|
||||
|
||||
@@ -58,6 +58,10 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
NOX_STD_INITIAL,
|
||||
gain_factor};
|
||||
}
|
||||
void start_measurement();
|
||||
void stop_measurement();
|
||||
void start_fan_cleaning();
|
||||
void activate_sht_heater();
|
||||
|
||||
protected:
|
||||
Sen6xType infer_type_from_product_name_(const std::string &product_name);
|
||||
@@ -65,6 +69,9 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
void finish_setup_();
|
||||
bool write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len);
|
||||
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
|
||||
bool command_blocked_() const;
|
||||
void set_command_wait_(uint32_t wait_ms);
|
||||
void on_measurement_started_();
|
||||
void poll_data_ready_();
|
||||
void read_measurements_();
|
||||
void parse_and_publish_measurements_();
|
||||
@@ -74,6 +81,11 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
optional<GasTuning> voc_tuning_params_;
|
||||
optional<GasTuning> nox_tuning_params_;
|
||||
Sen6xType sen6x_type_{UNKNOWN};
|
||||
// Post-command wait windows, stored as start time plus duration so elapsed-time checks survive millis() wrap
|
||||
uint32_t command_started_at_{0};
|
||||
uint32_t command_wait_{0};
|
||||
uint32_t co2_started_at_{0};
|
||||
uint32_t co2_wait_{0};
|
||||
uint16_t read_cmd_{0};
|
||||
uint8_t setup_step_index_{0};
|
||||
uint8_t firmware_version_major_{0};
|
||||
@@ -81,6 +93,7 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
|
||||
uint8_t poll_retries_remaining_{0};
|
||||
uint8_t read_words_{0};
|
||||
bool initialized_{false};
|
||||
bool measuring_{false};
|
||||
bool startup_complete_{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensirion_common, sensor
|
||||
from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX
|
||||
@@ -209,3 +211,21 @@ async def to_code(config: ConfigType) -> None:
|
||||
args.append(std_initial)
|
||||
args.append(tuning[CONF_GAIN_FACTOR])
|
||||
cg.add(getattr(var, setter)(*args))
|
||||
|
||||
|
||||
SEN6X_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(SEN6XComponent),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
for _name, _call in (
|
||||
("sen6x.start_measurement", "start_measurement()"),
|
||||
("sen6x.stop_measurement", "stop_measurement()"),
|
||||
("sen6x.start_fan_cleaning", "start_fan_cleaning()"),
|
||||
("sen6x.activate_sht_heater", "activate_sht_heater()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, SEN6X_ACTION_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
|
||||
@@ -17,12 +17,22 @@ CONF_CHARGE_CURRENT = "charge_current"
|
||||
CONF_PRECHARGE_CURRENT = "precharge_current"
|
||||
CONF_CHARGE_ENABLED = "charge_enabled"
|
||||
CONF_ENABLE_ADC = "enable_adc"
|
||||
CONF_I2C_WATCHDOG_TIMEOUT = "i2c_watchdog_timeout"
|
||||
|
||||
sy6970_ns = cg.esphome_ns.namespace("sy6970")
|
||||
SY6970Component = sy6970_ns.class_(
|
||||
"SY6970Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
SY6970Listener = sy6970_ns.class_("SY6970Listener")
|
||||
I2CWatchdogTimeout = sy6970_ns.enum("I2CWatchdogTimeout")
|
||||
|
||||
# REG07[5:4]; the component kicks the watchdog itself, so the chip's 40S default can stay on
|
||||
I2C_WATCHDOG_TIMEOUTS = {
|
||||
"DISABLED": I2CWatchdogTimeout.I2C_WATCHDOG_DISABLED,
|
||||
"40S": I2CWatchdogTimeout.I2C_WATCHDOG_40S,
|
||||
"80S": I2CWatchdogTimeout.I2C_WATCHDOG_80S,
|
||||
"160S": I2CWatchdogTimeout.I2C_WATCHDOG_160S,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
@@ -43,6 +53,9 @@ CONFIG_SCHEMA = (
|
||||
),
|
||||
cv.Optional(CONF_CHARGE_ENABLED, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_ADC, default=True): cv.boolean,
|
||||
cv.Optional(CONF_I2C_WATCHDOG_TIMEOUT, default="40S"): cv.enum(
|
||||
I2C_WATCHDOG_TIMEOUTS, upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("5s"))
|
||||
@@ -60,6 +73,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
config[CONF_PRECHARGE_CURRENT],
|
||||
config[CONF_CHARGE_ENABLED],
|
||||
config[CONF_ENABLE_ADC],
|
||||
config[CONF_I2C_WATCHDOG_TIMEOUT],
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -6,6 +6,25 @@ namespace esphome::sy6970 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "sy6970");
|
||||
|
||||
static const LogString *i2c_watchdog_timeout_to_string(I2CWatchdogTimeout timeout) {
|
||||
switch (timeout) {
|
||||
case I2C_WATCHDOG_DISABLED:
|
||||
return LOG_STR("Disabled");
|
||||
case I2C_WATCHDOG_40S:
|
||||
return LOG_STR("40s");
|
||||
case I2C_WATCHDOG_80S:
|
||||
return LOG_STR("80s");
|
||||
case I2C_WATCHDOG_160S:
|
||||
return LOG_STR("160s");
|
||||
default:
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr uint32_t I2C_WATCHDOG_KICK_INTERVAL_ID = 0;
|
||||
// Well under the 40 s default, so a failed kick or a timeout the chip did not accept still leaves margin
|
||||
static constexpr uint32_t I2C_WATCHDOG_KICK_INTERVAL_MS = 10000;
|
||||
|
||||
bool SY6970Component::read_all_registers_() {
|
||||
// Read all registers from 0x00 to 0x14 in one transaction (21 bytes)
|
||||
// This includes unused registers 0x0F, 0x10 for performance
|
||||
@@ -51,6 +70,11 @@ void SY6970Component::setup() {
|
||||
ESP_LOGW(TAG, "Unexpected chip ID: 0x%02X (expected 0x00)", chip_id);
|
||||
}
|
||||
|
||||
// Service the watchdog first, so the settings below are not reverted while they are written
|
||||
ESP_LOGV(TAG, "Setting I2C watchdog timeout to %s",
|
||||
LOG_STR_ARG(i2c_watchdog_timeout_to_string(this->i2c_watchdog_timeout_)));
|
||||
this->set_i2c_watchdog_timeout(this->i2c_watchdog_timeout_);
|
||||
|
||||
// Apply configuration options (all have defaults now)
|
||||
ESP_LOGV(TAG, "Setting LED enabled to %s", ONOFF(this->led_enabled_));
|
||||
this->set_led_enabled(this->led_enabled_);
|
||||
@@ -85,9 +109,11 @@ void SY6970Component::dump_config() {
|
||||
" Charge Current: %u mA\n"
|
||||
" Precharge Current: %u mA\n"
|
||||
" Charge Enabled: %s\n"
|
||||
" ADC Enabled: %s",
|
||||
" ADC Enabled: %s\n"
|
||||
" I2C Watchdog Timeout: %s",
|
||||
ONOFF(this->led_enabled_), this->input_current_limit_, this->charge_voltage_, this->charge_current_,
|
||||
this->precharge_current_, ONOFF(this->charge_enabled_), ONOFF(this->enable_adc_));
|
||||
this->precharge_current_, ONOFF(this->charge_enabled_), ONOFF(this->enable_adc_),
|
||||
LOG_STR_ARG(i2c_watchdog_timeout_to_string(this->i2c_watchdog_timeout_)));
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
if (this->is_failed()) {
|
||||
@@ -198,4 +224,44 @@ void SY6970Component::set_enable_adc_measure(bool enabled) {
|
||||
this->update_register_(SY6970_REG_ADC_CONTROL, 0xC0, enabled ? 0xC0 : 0x00);
|
||||
}
|
||||
|
||||
void SY6970Component::set_i2c_watchdog_timeout(I2CWatchdogTimeout timeout) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
if (timeout != I2C_WATCHDOG_DISABLED) {
|
||||
// Changing the timeout does not restart the chip's timer, which may be close to expiring
|
||||
this->reset_i2c_watchdog();
|
||||
}
|
||||
|
||||
// REG07 bits 5:4 (WATCHDOG[1:0])
|
||||
bool written = this->update_register_(SY6970_REG_TIMER_CONTROL, 0x30, static_cast<uint8_t>(timeout) << 4);
|
||||
if (!written) {
|
||||
ESP_LOGW(TAG, "Failed to set I2C watchdog timeout to %s", LOG_STR_ARG(i2c_watchdog_timeout_to_string(timeout)));
|
||||
}
|
||||
|
||||
if (timeout == I2C_WATCHDOG_DISABLED && written) {
|
||||
this->i2c_watchdog_timeout_ = timeout;
|
||||
this->cancel_interval(I2C_WATCHDOG_KICK_INTERVAL_ID);
|
||||
return;
|
||||
}
|
||||
if (timeout != I2C_WATCHDOG_DISABLED) {
|
||||
this->i2c_watchdog_timeout_ = timeout;
|
||||
}
|
||||
// Keeps kicking after a failed write too: the chip may still be on its 40 s default
|
||||
this->set_interval(I2C_WATCHDOG_KICK_INTERVAL_ID, I2C_WATCHDOG_KICK_INTERVAL_MS,
|
||||
[this]() { this->reset_i2c_watchdog(); });
|
||||
}
|
||||
|
||||
bool SY6970Component::reset_i2c_watchdog() {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
// WD_RST, self-clearing
|
||||
if (!this->update_register_(SY6970_REG_SYS_CONTROL, 0x40, 0x40)) {
|
||||
ESP_LOGW(TAG, "I2C watchdog reset failed, settings may revert");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::sy6970
|
||||
|
||||
@@ -62,6 +62,15 @@ enum ChargeStatus {
|
||||
CHARGE_STATUS_CHARGE_DONE = 3,
|
||||
};
|
||||
|
||||
// REG07[5:4]. When the watchdog elapses the chip reverts CHG_CONFIG and STAT_DIS to their defaults,
|
||||
// so an enabled watchdog is kicked on its own interval.
|
||||
enum I2CWatchdogTimeout {
|
||||
I2C_WATCHDOG_DISABLED = 0,
|
||||
I2C_WATCHDOG_40S = 1,
|
||||
I2C_WATCHDOG_80S = 2,
|
||||
I2C_WATCHDOG_160S = 3,
|
||||
};
|
||||
|
||||
// Structure to hold all register data read in one transaction
|
||||
struct SY6970Data {
|
||||
uint8_t registers[21]; // Registers 0x00-0x14 (includes unused 0x0F, 0x10)
|
||||
@@ -76,14 +85,16 @@ class SY6970Listener {
|
||||
class SY6970Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
SY6970Component(bool led_enabled, uint16_t input_current_limit, uint16_t charge_voltage, uint16_t charge_current,
|
||||
uint16_t precharge_current, bool charge_enabled, bool enable_adc)
|
||||
uint16_t precharge_current, bool charge_enabled, bool enable_adc,
|
||||
I2CWatchdogTimeout i2c_watchdog_timeout)
|
||||
: led_enabled_(led_enabled),
|
||||
input_current_limit_(input_current_limit),
|
||||
charge_voltage_(charge_voltage),
|
||||
charge_current_(charge_current),
|
||||
precharge_current_(precharge_current),
|
||||
charge_enabled_(charge_enabled),
|
||||
enable_adc_(enable_adc) {}
|
||||
enable_adc_(enable_adc),
|
||||
i2c_watchdog_timeout_(i2c_watchdog_timeout) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
@@ -99,6 +110,10 @@ class SY6970Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_charge_enabled(bool enabled);
|
||||
void set_led_enabled(bool enabled);
|
||||
void set_enable_adc_measure(bool enabled = true);
|
||||
void set_i2c_watchdog_timeout(I2CWatchdogTimeout timeout);
|
||||
|
||||
/// Kick the I2C watchdog (REG03 bit 6); runs on its own interval while enabled. Returns false when not written.
|
||||
bool reset_i2c_watchdog();
|
||||
|
||||
protected:
|
||||
bool read_all_registers_();
|
||||
@@ -116,6 +131,7 @@ class SY6970Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
uint16_t precharge_current_;
|
||||
bool charge_enabled_;
|
||||
bool enable_adc_;
|
||||
I2CWatchdogTimeout i2c_watchdog_timeout_;
|
||||
};
|
||||
|
||||
} // namespace esphome::sy6970
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import datetime
|
||||
import datetime as dt
|
||||
import random
|
||||
|
||||
import esphome.codegen as cg
|
||||
@@ -176,7 +176,7 @@ async def _attr_to_code(config: ConfigType) -> None:
|
||||
basic_attrs.date_code,
|
||||
# Local build time, matching the esp32 implementation
|
||||
# (App.get_build_time() in C++).
|
||||
datetime.datetime.now().astimezone().strftime("%Y%m%d %H%M%S"),
|
||||
dt.datetime.now().astimezone().strftime("%Y%m%d %H%M%S"),
|
||||
),
|
||||
zigbee_assign(
|
||||
basic_attrs.power_source,
|
||||
|
||||
@@ -217,6 +217,7 @@
|
||||
#define TCA8418_LISTENER_COUNT 1
|
||||
#define USE_MEDIA_PLAYER
|
||||
#define USE_MEDIA_SOURCE
|
||||
#define NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT 1
|
||||
#define USE_NETWORK
|
||||
#define USE_NETWORK_DEFAULT_ROUTE
|
||||
#define USE_NETWORK_PRIMARY_INTERFACE_WIFI
|
||||
@@ -567,6 +568,10 @@
|
||||
#endif
|
||||
#ifndef USE_OPENTHREAD
|
||||
#define USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
#ifdef USE_ETHERNET
|
||||
#define USE_MDNS_ACTIVE_INTERFACE_LISTENER
|
||||
#define USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -647,6 +652,7 @@
|
||||
#define USE_ETHERNET_SPI
|
||||
#endif
|
||||
#define USE_ETHERNET_W5500
|
||||
#define USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
#define USE_WIFI_IP_STATE_LISTENERS
|
||||
#define ESPHOME_WIFI_IP_STATE_LISTENERS 2
|
||||
#define USE_ETHERNET_IP_STATE_LISTENERS
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@ tzlocal==5.4.4 # from time
|
||||
tzdata>=2026.5 # from time
|
||||
pyserial==3.5
|
||||
platformio==6.2.0
|
||||
esptool==5.4.0
|
||||
esptool==5.5.0
|
||||
click==8.5.0
|
||||
aioesphomeapi==46.10.0
|
||||
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
|
||||
@@ -19,7 +19,7 @@ puremagic==2.2.0
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
backports.zstd==1.7.0; python_version < "3.14" # esphome.helpers.zstd_module; compression.zstd is in the standard library from 3.14
|
||||
backports.zstd==1.8.0; python_version < "3.14" # esphome.helpers.zstd_module; compression.zstd is in the standard library from 3.14
|
||||
pillow==12.3.0
|
||||
resvg-py==0.5.0
|
||||
freetype-py==2.5.1
|
||||
@@ -31,9 +31,9 @@ smp==4.2.0; platform_system != "Darwin" or platform_machine != "x86_64"
|
||||
smp==4.1.0; platform_system == "Darwin" and platform_machine == "x86_64"
|
||||
requests==2.34.2
|
||||
py7zr==1.1.3
|
||||
platformdirs==4.12.3 # native esp-idf toolchain global cache dir
|
||||
platformdirs==4.12.4 # native esp-idf toolchain global cache dir
|
||||
ninja==1.13.2 # native host and esp8266 arduino toolchain build driver
|
||||
filelock==4.0.12 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
|
||||
filelock==4.1.0 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
|
||||
|
||||
# esp-idf >= 5.0 requires this
|
||||
pyparsing >= 3.3.3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pylint==4.1.2
|
||||
flake8==7.4.1 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
ruff==0.16.10 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
pyupgrade==3.21.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
ruff==0.17.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
pyupgrade==3.22.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
prek==0.5.5 # .github/workflows/ci.yml reads this pin
|
||||
yamlrocks==0.6.1 # used by script/sync_dependency_versions.py
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride, add_defines_to_code
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# Loops that only exist on boards with PSRAM are gated on this define;
|
||||
# set it so the host benchmark measures the same code those boards run.
|
||||
add_defines_to_code(manifest, "USE_PSRAM")
|
||||
@@ -0,0 +1,82 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
|
||||
namespace esphome::display::benchmarks {
|
||||
|
||||
static constexpr int kWidth = 128;
|
||||
static constexpr int kHeight = 96;
|
||||
// Square frame so a rotated block stays inside it.
|
||||
static constexpr int kStride = kWidth > kHeight ? kWidth : kHeight;
|
||||
|
||||
// A buffered display without its own draw_pixels_at(), so every block goes
|
||||
// through the base class fallback and the DisplayBuffer pixel path.
|
||||
class BenchDisplay : public DisplayBuffer {
|
||||
public:
|
||||
BenchDisplay() : frame_(std::make_unique<uint8_t[]>(kStride * kStride)) {}
|
||||
void update() override {}
|
||||
DisplayType get_display_type() override { return DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
int get_width_internal() override { return kStride; }
|
||||
int get_height_internal() override { return kStride; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override { this->frame_[y * kStride + x] = color.r; }
|
||||
|
||||
std::unique_ptr<uint8_t[]> frame_;
|
||||
};
|
||||
|
||||
// Deterministic pseudo random pixel bytes, sized for the widest format.
|
||||
static std::unique_ptr<uint8_t[]> make_pixels() {
|
||||
constexpr size_t bytes = kWidth * kHeight * 3;
|
||||
auto data = std::make_unique<uint8_t[]>(bytes);
|
||||
uint32_t seed = 11897;
|
||||
for (size_t i = 0; i < bytes; i++) {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
data[i] = static_cast<uint8_t>(seed >> 16);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// One full block the way LVGL hands a display its rendering.
|
||||
static void draw_block(benchmark::State &state, ColorBitness bitness, bool big_endian,
|
||||
DisplayRotation rotation = DISPLAY_ROTATION_0_DEGREES) {
|
||||
auto data = make_pixels();
|
||||
BenchDisplay display;
|
||||
display.set_rotation(rotation);
|
||||
for (auto _ : state) {
|
||||
display.draw_pixels_at(0, 0, kWidth, kHeight, data.get(), COLOR_ORDER_RGB, bitness, big_endian, 0, 0, 0);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kWidth * kHeight);
|
||||
}
|
||||
|
||||
static void DrawPixelsAt_RGB565_BigEndian(benchmark::State &state) { draw_block(state, COLOR_BITNESS_565, true); }
|
||||
BENCHMARK(DrawPixelsAt_RGB565_BigEndian);
|
||||
|
||||
static void DrawPixelsAt_RGB565_LittleEndian(benchmark::State &state) { draw_block(state, COLOR_BITNESS_565, false); }
|
||||
BENCHMARK(DrawPixelsAt_RGB565_LittleEndian);
|
||||
|
||||
static void DrawPixelsAt_RGB888(benchmark::State &state) { draw_block(state, COLOR_BITNESS_888, true); }
|
||||
BENCHMARK(DrawPixelsAt_RGB888);
|
||||
|
||||
static void DrawPixelsAt_RGB332(benchmark::State &state) { draw_block(state, COLOR_BITNESS_332, true); }
|
||||
BENCHMARK(DrawPixelsAt_RGB332);
|
||||
|
||||
static void DrawPixelsAt_RGB565_Rotated(benchmark::State &state) {
|
||||
draw_block(state, COLOR_BITNESS_565, true, DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(DrawPixelsAt_RGB565_Rotated);
|
||||
|
||||
static void DrawPixelsAt_RGB888_Rotated(benchmark::State &state) {
|
||||
draw_block(state, COLOR_BITNESS_888, true, DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(DrawPixelsAt_RGB888_Rotated);
|
||||
|
||||
static void DrawPixelsAt_RGB332_Rotated(benchmark::State &state) {
|
||||
draw_block(state, COLOR_BITNESS_332, true, DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(DrawPixelsAt_RGB332_Rotated);
|
||||
|
||||
} // namespace esphome::display::benchmarks
|
||||
@@ -0,0 +1,7 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride, add_defines_to_code
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# Loops that only exist on boards with PSRAM are gated on this define;
|
||||
# set it so the host benchmark measures the same code those boards run.
|
||||
add_defines_to_code(manifest, "USE_PSRAM")
|
||||
@@ -0,0 +1,245 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/image/image.h"
|
||||
|
||||
namespace esphome::image::benchmarks {
|
||||
|
||||
static constexpr int kWidth = 128;
|
||||
static constexpr int kHeight = 96;
|
||||
// Square frame so a rotated draw of the image stays inside it.
|
||||
static constexpr int kStride = kWidth > kHeight ? kWidth : kHeight;
|
||||
|
||||
// One byte per pixel frame buffer with no hardware behind it, so the
|
||||
// measurement is Image::draw() plus the DisplayBuffer pixel path every
|
||||
// buffered display shares. Everything fits the host cache, so this counts
|
||||
// the per pixel work, not what a frame buffer in PSRAM costs.
|
||||
class BenchDisplay : public display::DisplayBuffer {
|
||||
public:
|
||||
BenchDisplay() : frame_(std::make_unique<uint8_t[]>(kStride * kStride)) {}
|
||||
void update() override {}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
int get_width_internal() override { return kStride; }
|
||||
int get_height_internal() override { return kStride; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override { this->frame_[y * kStride + x] = color.r; }
|
||||
|
||||
std::unique_ptr<uint8_t[]> frame_;
|
||||
|
||||
public:
|
||||
uint8_t pixel(int x, int y) const { return this->frame_[y * kStride + x]; }
|
||||
};
|
||||
|
||||
// Models a driver with a bulk path: rows of little endian RGB565 are copied
|
||||
// straight into a 16 bit frame, as the TFT drivers do for their own format.
|
||||
// Image::draw() only reaches it once it hands opaque images to
|
||||
// draw_pixels_at(); until then this case matches ImageDraw_RGB565.
|
||||
class BulkBenchDisplay : public BenchDisplay {
|
||||
public:
|
||||
BulkBenchDisplay() : frame565_(std::make_unique<uint16_t[]>(kStride * kStride)) {}
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override {
|
||||
if (bitness != display::COLOR_BITNESS_565 || big_endian) {
|
||||
BenchDisplay::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
}
|
||||
const size_t stride = x_offset + w + x_pad;
|
||||
for (int y = 0; y < h; y++) {
|
||||
memcpy(this->frame565_.get() + (y_start + y) * kStride + x_start, ptr + ((y_offset + y) * stride + x_offset) * 2,
|
||||
w * 2);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
std::unique_ptr<uint16_t[]> frame565_;
|
||||
};
|
||||
|
||||
// Draws the image at rotation 0 and 90 and checks the second frame is the
|
||||
// transpose of the first, which is what a wrong traversal order would break.
|
||||
static bool frames_match(Image &image) {
|
||||
BenchDisplay plain;
|
||||
BenchDisplay rotated;
|
||||
rotated.set_rotation(display::DISPLAY_ROTATION_90_DEGREES);
|
||||
image.draw(0, 0, &plain, display::COLOR_ON, display::COLOR_OFF);
|
||||
image.draw(0, 0, &rotated, display::COLOR_ON, display::COLOR_OFF);
|
||||
for (int y = 0; y < kHeight; y++) {
|
||||
for (int x = 0; x < kWidth; x++) {
|
||||
// DisplayBuffer maps (x, y) to (width - y - 1, x) at 90 degrees
|
||||
if (plain.pixel(x, y) != rotated.pixel(kStride - y - 1, x))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Deterministic pseudo random pixel bytes so the alpha branch sees a mix of
|
||||
// pixels. Sized for the widest format, the image reads only what it needs.
|
||||
static std::unique_ptr<uint8_t[]> make_pixels() {
|
||||
constexpr size_t bytes = kWidth * kHeight * 4;
|
||||
auto data = std::make_unique<uint8_t[]>(bytes);
|
||||
uint32_t seed = 11897;
|
||||
for (size_t i = 0; i < bytes; i++) {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
data[i] = static_cast<uint8_t>(seed >> 16);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
static void draw_image(benchmark::State &state, ImageType type, Transparency transparency,
|
||||
display::DisplayRotation rotation = display::DISPLAY_ROTATION_0_DEGREES) {
|
||||
auto data = make_pixels();
|
||||
Image image(data.get(), kWidth, kHeight, type, transparency);
|
||||
BenchDisplay display;
|
||||
display.set_rotation(rotation);
|
||||
if (!frames_match(image)) {
|
||||
state.SkipWithError("rotated draw is not the transpose of the plain draw");
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto _ : state) {
|
||||
image.draw(0, 0, &display, display::COLOR_ON, display::COLOR_OFF);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kWidth * kHeight);
|
||||
}
|
||||
|
||||
static void ImageDraw_Binary(benchmark::State &state) { draw_image(state, IMAGE_TYPE_BINARY, TRANSPARENCY_OPAQUE); }
|
||||
BENCHMARK(ImageDraw_Binary);
|
||||
|
||||
static void ImageDraw_Grayscale(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_OPAQUE);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Grayscale);
|
||||
|
||||
static void ImageDraw_RGB565(benchmark::State &state) { draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE); }
|
||||
BENCHMARK(ImageDraw_RGB565);
|
||||
|
||||
static void ImageDraw_RGB(benchmark::State &state) { draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_OPAQUE); }
|
||||
BENCHMARK(ImageDraw_RGB);
|
||||
|
||||
// The same opaque RGB565 image on a display with a bulk path.
|
||||
static void ImageDraw_RGB565_Bulk(benchmark::State &state) {
|
||||
auto data = make_pixels();
|
||||
Image image(data.get(), kWidth, kHeight, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE);
|
||||
BulkBenchDisplay display;
|
||||
for (auto _ : state) {
|
||||
image.draw(0, 0, &display, display::COLOR_ON, display::COLOR_OFF);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kWidth * kHeight);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB565_Bulk);
|
||||
|
||||
static void ImageDraw_RGB_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_OPAQUE, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB_Rotated);
|
||||
|
||||
static void ImageDraw_Binary_Transparent(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_BINARY, TRANSPARENCY_CHROMA_KEY);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Binary_Transparent);
|
||||
|
||||
static void ImageDraw_Grayscale_ChromaKey(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_CHROMA_KEY);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Grayscale_ChromaKey);
|
||||
|
||||
static void ImageDraw_Grayscale_Alpha(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_ALPHA_CHANNEL);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Grayscale_Alpha);
|
||||
|
||||
static void ImageDraw_RGB_Alpha(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_ALPHA_CHANNEL);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB_Alpha);
|
||||
|
||||
static void ImageDraw_RGB_ChromaKey(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_CHROMA_KEY);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB_ChromaKey);
|
||||
|
||||
static void ImageDraw_RGB565_ChromaKey(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_CHROMA_KEY);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB565_ChromaKey);
|
||||
|
||||
static void ImageDraw_RGB565_Alpha(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_ALPHA_CHANNEL);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB565_Alpha);
|
||||
|
||||
static void ImageDraw_RGB111(benchmark::State &state) { draw_image(state, IMAGE_TYPE_RGB111, TRANSPARENCY_OPAQUE); }
|
||||
BENCHMARK(ImageDraw_RGB111);
|
||||
|
||||
static void ImageDraw_RGB111_Alpha(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB111, TRANSPARENCY_ALPHA_CHANNEL);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB111_Alpha);
|
||||
|
||||
// The same cases with the display rotated 90 degrees, which walks the other loop order.
|
||||
static void ImageDraw_Binary_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_BINARY, TRANSPARENCY_OPAQUE, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Binary_Rotated);
|
||||
|
||||
static void ImageDraw_Grayscale_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_OPAQUE, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Grayscale_Rotated);
|
||||
|
||||
static void ImageDraw_RGB565_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB565_Rotated);
|
||||
|
||||
static void ImageDraw_Binary_Transparent_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_BINARY, TRANSPARENCY_CHROMA_KEY, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Binary_Transparent_Rotated);
|
||||
|
||||
static void ImageDraw_Grayscale_ChromaKey_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_CHROMA_KEY, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Grayscale_ChromaKey_Rotated);
|
||||
|
||||
static void ImageDraw_Grayscale_Alpha_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_ALPHA_CHANNEL, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_Grayscale_Alpha_Rotated);
|
||||
|
||||
static void ImageDraw_RGB_Alpha_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_ALPHA_CHANNEL, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB_Alpha_Rotated);
|
||||
|
||||
static void ImageDraw_RGB_ChromaKey_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB, TRANSPARENCY_CHROMA_KEY, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB_ChromaKey_Rotated);
|
||||
|
||||
static void ImageDraw_RGB565_ChromaKey_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_CHROMA_KEY, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB565_ChromaKey_Rotated);
|
||||
|
||||
static void ImageDraw_RGB565_Alpha_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB565, TRANSPARENCY_ALPHA_CHANNEL, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB565_Alpha_Rotated);
|
||||
|
||||
static void ImageDraw_RGB111_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB111, TRANSPARENCY_OPAQUE, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB111_Rotated);
|
||||
|
||||
static void ImageDraw_RGB111_Alpha_Rotated(benchmark::State &state) {
|
||||
draw_image(state, IMAGE_TYPE_RGB111, TRANSPARENCY_ALPHA_CHANNEL, display::DISPLAY_ROTATION_90_DEGREES);
|
||||
}
|
||||
BENCHMARK(ImageDraw_RGB111_Alpha_Rotated);
|
||||
|
||||
} // namespace esphome::image::benchmarks
|
||||
@@ -0,0 +1,125 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
namespace esphome::runtime_image::benchmarks {
|
||||
|
||||
static constexpr int kWidth = 128;
|
||||
static constexpr int kHeight = 96;
|
||||
|
||||
// One byte per pixel frame buffer with no hardware behind it, so the
|
||||
// measurement is the image draw plus the DisplayBuffer pixel path every
|
||||
// buffered display shares.
|
||||
class BenchDisplay : public display::DisplayBuffer {
|
||||
public:
|
||||
BenchDisplay() : frame_(std::make_unique<uint8_t[]>(kWidth * kHeight)) {}
|
||||
void update() override {}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
int get_width_internal() override { return kWidth; }
|
||||
int get_height_internal() override { return kHeight; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override { this->frame_[y * kWidth + x] = color.r; }
|
||||
|
||||
std::unique_ptr<uint8_t[]> frame_;
|
||||
};
|
||||
|
||||
// A runtime image filled the way a decoder fills it, one draw_pixel() per
|
||||
// pixel, then published as decoded so draw() uses the buffer.
|
||||
class BenchImage : public RuntimeImage {
|
||||
public:
|
||||
BenchImage(image::ImageType type, image::Transparency transparency, bool big_endian)
|
||||
: RuntimeImage(JPEG, type, transparency, nullptr, big_endian, kWidth, kHeight) {}
|
||||
|
||||
bool fill() {
|
||||
if (this->resize(kWidth, kHeight) == 0)
|
||||
return false;
|
||||
uint32_t seed = 11897;
|
||||
for (int y = 0; y < kHeight; y++) {
|
||||
for (int x = 0; x < kWidth; x++) {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
// Every fourth pixel is transparent so the chroma key cases hit both branches
|
||||
const uint8_t alpha = (x & 3) == 0 ? 0x00 : 0xFF;
|
||||
this->draw_pixel(x, y, Color(seed >> 24, seed >> 16, seed >> 8, alpha));
|
||||
}
|
||||
}
|
||||
this->width_ = this->buffer_width_;
|
||||
this->height_ = this->buffer_height_;
|
||||
this->data_start_ = this->buffer_;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// The decoder side: writing every pixel of the image into its buffer.
|
||||
static void fill_image(benchmark::State &state, image::ImageType type, image::Transparency transparency) {
|
||||
BenchImage image(type, transparency, false);
|
||||
for (auto _ : state) {
|
||||
if (!image.fill()) {
|
||||
state.SkipWithError("buffer allocation failed");
|
||||
return;
|
||||
}
|
||||
benchmark::DoNotOptimize(image.get_data_start());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kWidth * kHeight);
|
||||
}
|
||||
|
||||
// The display side: drawing the decoded image onto a buffered display.
|
||||
static void draw_image(benchmark::State &state, image::ImageType type, image::Transparency transparency,
|
||||
bool big_endian = false) {
|
||||
BenchImage image(type, transparency, big_endian);
|
||||
if (!image.fill()) {
|
||||
state.SkipWithError("buffer allocation failed");
|
||||
return;
|
||||
}
|
||||
BenchDisplay display;
|
||||
for (auto _ : state) {
|
||||
image.draw(0, 0, &display, display::COLOR_ON, display::COLOR_OFF);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kWidth * kHeight);
|
||||
}
|
||||
|
||||
static void RuntimeImageFill_RGB565(benchmark::State &state) {
|
||||
fill_image(state, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);
|
||||
}
|
||||
BENCHMARK(RuntimeImageFill_RGB565);
|
||||
|
||||
static void RuntimeImageFill_RGB565_ChromaKey(benchmark::State &state) {
|
||||
fill_image(state, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_CHROMA_KEY);
|
||||
}
|
||||
BENCHMARK(RuntimeImageFill_RGB565_ChromaKey);
|
||||
|
||||
static void RuntimeImageFill_RGB(benchmark::State &state) {
|
||||
fill_image(state, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
}
|
||||
BENCHMARK(RuntimeImageFill_RGB);
|
||||
|
||||
static void RuntimeImageDraw_RGB565(benchmark::State &state) {
|
||||
draw_image(state, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);
|
||||
}
|
||||
BENCHMARK(RuntimeImageDraw_RGB565);
|
||||
|
||||
static void RuntimeImageDraw_RGB565_BigEndian(benchmark::State &state) {
|
||||
draw_image(state, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE, true);
|
||||
}
|
||||
BENCHMARK(RuntimeImageDraw_RGB565_BigEndian);
|
||||
|
||||
static void RuntimeImageDraw_RGB565_ChromaKey(benchmark::State &state) {
|
||||
draw_image(state, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_CHROMA_KEY);
|
||||
}
|
||||
BENCHMARK(RuntimeImageDraw_RGB565_ChromaKey);
|
||||
|
||||
static void RuntimeImageDraw_RGB(benchmark::State &state) {
|
||||
draw_image(state, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
}
|
||||
BENCHMARK(RuntimeImageDraw_RGB);
|
||||
|
||||
static void RuntimeImageDraw_Grayscale(benchmark::State &state) {
|
||||
draw_image(state, image::IMAGE_TYPE_GRAYSCALE, image::TRANSPARENCY_OPAQUE);
|
||||
}
|
||||
BENCHMARK(RuntimeImageDraw_Grayscale);
|
||||
|
||||
} // namespace esphome::runtime_image::benchmarks
|
||||
@@ -0,0 +1,28 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: 19
|
||||
mosi_pin: 21
|
||||
miso_pin: 23
|
||||
cs_pin: 18
|
||||
interrupt_pin: 36
|
||||
reset_pin: 22
|
||||
clock_speed: 10Mhz
|
||||
|
||||
network:
|
||||
priority:
|
||||
- ethernet
|
||||
- wifi
|
||||
|
||||
api:
|
||||
@@ -0,0 +1,26 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
rp2:
|
||||
board: rpipicow
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: 18
|
||||
mosi_pin: 19
|
||||
miso_pin: 16
|
||||
cs_pin: 17
|
||||
interrupt_pin: 21
|
||||
reset_pin: 20
|
||||
mac_address: "02:AA:BB:CC:DD:01"
|
||||
|
||||
network:
|
||||
priority:
|
||||
- ethernet
|
||||
- wifi
|
||||
|
||||
api:
|
||||
@@ -0,0 +1,52 @@
|
||||
"""mDNS follows the interface holding the default route through an
|
||||
ActiveInterfaceListener, only where NetworkComponent tracks that interface
|
||||
(ESP32 with more than one interface) and the network TXT record exists."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
DYNAMIC_TXT_DEFINE = "USE_MDNS_DYNAMIC_NETWORK_TXT"
|
||||
LISTENER_DEFINES = {
|
||||
"USE_MDNS_ACTIVE_INTERFACE_LISTENER",
|
||||
"NETWORK_ACTIVE_INTERFACE_LISTENER_COUNT",
|
||||
}
|
||||
|
||||
|
||||
def _defines() -> set[str]:
|
||||
return {define.name for define in CORE.defines}
|
||||
|
||||
|
||||
def test_mdns_registers_on_esp32_with_wifi_ethernet_and_api(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("priority_ethernet_first_api.yaml"))
|
||||
|
||||
assert "add_active_interface_listener(" in main_cpp
|
||||
assert _defines() >= LISTENER_DEFINES | {DYNAMIC_TXT_DEFINE}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fixture", "dynamic_txt"),
|
||||
[
|
||||
# No api means no _esphomelib service, so no network TXT record
|
||||
pytest.param("priority_ethernet_first.yaml", False, id="esp32_without_api"),
|
||||
# No route arbitration on RP2; mDNS keeps its periodic check there
|
||||
pytest.param("priority_rp2040_api.yaml", True, id="rp2040"),
|
||||
],
|
||||
)
|
||||
def test_mdns_does_not_register(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
fixture: str,
|
||||
dynamic_txt: bool,
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path(fixture))
|
||||
|
||||
assert "add_active_interface_listener(" not in main_cpp
|
||||
assert not LISTENER_DEFINES & _defines()
|
||||
assert (DYNAMIC_TXT_DEFINE in _defines()) is dynamic_txt
|
||||
@@ -87,6 +87,40 @@ display:
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: waveshare-2.13in-g
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: waveshare-2.13in-g-v1
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
|
||||
- platform: epaper_spi
|
||||
model: seeed-reterminal-e1002
|
||||
- platform: epaper_spi
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
esp32:
|
||||
framework:
|
||||
advanced:
|
||||
minimum_chip_revision: "2.0"
|
||||
|
||||
canbus:
|
||||
- platform: esp32_can
|
||||
rx_pin: GPIO14
|
||||
tx_pin: GPIO12
|
||||
can_id: 4
|
||||
bit_rate: 20kbps
|
||||
@@ -0,0 +1,6 @@
|
||||
climate:
|
||||
- platform: midea
|
||||
id: midea_uart_only
|
||||
name: Midea Climate UART Only
|
||||
autoconf: true
|
||||
display_control: UART
|
||||
@@ -33,6 +33,7 @@ climate:
|
||||
timeout: 2s
|
||||
beeper: false
|
||||
autoconf: true
|
||||
display_control: AUTO
|
||||
visual:
|
||||
min_temperature: 17 °C
|
||||
max_temperature: 30 °C
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
midea: !include common-uart_only.yaml
|
||||
|
||||
# The MideaUART library includes esp_wifi.h on ESP-IDF
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
@@ -48,3 +48,17 @@ sensor:
|
||||
formaldehyde:
|
||||
name: Formaldehyde
|
||||
address: 0x6B
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Fan Cleaning
|
||||
on_press:
|
||||
- sen6x.stop_measurement: sen6x_sensor
|
||||
- delay: 2s
|
||||
- sen6x.start_fan_cleaning: sen6x_sensor
|
||||
- delay: 11s
|
||||
- sen6x.start_measurement: sen6x_sensor
|
||||
- platform: template
|
||||
name: SHT Heater
|
||||
on_press:
|
||||
- sen6x.activate_sht_heater: sen6x_sensor
|
||||
|
||||
@@ -9,6 +9,7 @@ sy6970:
|
||||
precharge_current: 128
|
||||
charge_enabled: true
|
||||
enable_adc: true
|
||||
i2c_watchdog_timeout: 80S
|
||||
update_interval: 5s
|
||||
|
||||
sensor:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome.loader import ComponentManifest, _replace_component_manifest
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
class ComponentManifestOverride:
|
||||
@@ -61,3 +62,24 @@ def set_testing_manifest(domain: str, manifest: ComponentManifestOverride) -> No
|
||||
directory provides an ``override_manifest`` function.
|
||||
"""
|
||||
_replace_component_manifest(domain, manifest)
|
||||
|
||||
|
||||
def add_defines_to_code(manifest: ComponentManifestOverride, *defines: str) -> None:
|
||||
"""Keep the component's real ``to_code`` and add the given defines after it.
|
||||
|
||||
For host test and benchmark builds of code that is gated on a platform
|
||||
define, such as loops that only exist on boards with PSRAM.
|
||||
"""
|
||||
from esphome import codegen as cg
|
||||
|
||||
manifest.enable_codegen()
|
||||
original_to_code = manifest.to_code
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await original_to_code(config)
|
||||
for define in defines:
|
||||
cg.add_define(define)
|
||||
|
||||
if hasattr(original_to_code, "priority"):
|
||||
to_code.priority = original_to_code.priority
|
||||
manifest.to_code = to_code
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Tests for ESP32 CAN bitrate validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT
|
||||
from esphome.components.esp32_can import canbus
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("revision", "supported"),
|
||||
[
|
||||
(None, False),
|
||||
("0.0", False),
|
||||
("1.0", False),
|
||||
("1.1", False),
|
||||
("2.0", True),
|
||||
("3.0", True),
|
||||
("3.1", True),
|
||||
],
|
||||
)
|
||||
def test_esp32_20kbps_minimum_revision(revision: str | None, supported: bool) -> None:
|
||||
"""Require the chip revision needed by the ESP-IDF timing macro."""
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: "ESP32"}
|
||||
advanced = {} if revision is None else {"minimum_chip_revision": revision}
|
||||
token = fv.full_config.set({"esp32": {"framework": {"advanced": advanced}}})
|
||||
try:
|
||||
config = {"bit_rate": canbus.validate_bit_rate("20kbps")}
|
||||
if supported:
|
||||
canbus.FINAL_VALIDATE_SCHEMA(config)
|
||||
else:
|
||||
with pytest.raises(cv.Invalid, match="minimum_chip_revision") as error:
|
||||
canbus.FINAL_VALIDATE_SCHEMA(config)
|
||||
assert error.value.path == ["bit_rate"]
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("variant", ["ESP32", "ESP32C3", "ESP32S3"])
|
||||
def test_other_bitrates(variant: str) -> None:
|
||||
"""Existing bitrates must not require a minimum chip revision."""
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: variant}
|
||||
config = {"bit_rate": canbus.validate_bit_rate("25kbps")}
|
||||
canbus.FINAL_VALIDATE_SCHEMA(config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("variant", ["ESP32C3", "ESP32S2", "ESP32S3", "ESP32C6"])
|
||||
def test_other_variants_20kbps(variant: str) -> None:
|
||||
"""Other supported ESP32 variants must retain 20 kbit/s support."""
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: variant}
|
||||
config = {"bit_rate": canbus.validate_bit_rate("20kbps")}
|
||||
canbus.FINAL_VALIDATE_SCHEMA(config)
|
||||
Reference in New Issue
Block a user