Merge branch 'dev' into platformio-prefetch-git-clones

# Conflicts:
#	esphome/platformio/prefetch.py
This commit is contained in:
J. Nick Koston
2026-08-31 13:23:59 -05:00
175 changed files with 3960 additions and 1034 deletions
@@ -0,0 +1,50 @@
name: Cache clang-tidy idedata
description: >
Cache the clang-tidy idedata and the headers it references under .temp
(headers only, about 30MB per env). Run after restore-python and cache-esp-idf.
inputs:
environment:
description: 'clang-tidy environment (e.g. esp32-idf-tidy).'
required: true
runs:
using: composite
steps:
- name: Compute cache key
id: key
shell: bash
run: |
. venv/bin/activate
[ -n "${{ inputs.environment }}" ] || { echo "::error::cache-clang-tidy-idedata: 'environment' input is empty"; exit 1; }
hash=$(python -c 'import sys; sys.path.insert(0, "script"); from clang_tidy_hash import idedata_cache_hash; print(idedata_cache_hash("${{ inputs.environment }}"))')
pyver=$(python -c 'import platform; print(platform.python_version())')
# Generating idedata is what installs ESP-IDF; never skip it over a missing
# install. This also skips the save, so a dev run that installs ESP-IDF
# warms the idedata cache on the next run.
if [ -d ~/.esphome-idf/frameworks ]; then
echo "skip=false" >> "$GITHUB_OUTPUT"
else
echo "ESP-IDF install missing, not using the clang-tidy idedata cache"
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
echo "key=${{ runner.os }}-tidy-idedata-${{ inputs.environment }}-$hash-py$pyver" >> "$GITHUB_OUTPUT"
{
echo "path<<EOF"
printf '%s\n' '.temp/idedata-*.json' '.temp/idedata-*.hash'
printf '.temp/**/*.%s\n' h hpp hh hxx inc inl ipp tpp
echo "EOF"
} >> "$GITHUB_OUTPUT"
# Mirror cache-esp-idf: write on dev, restore-only on PRs. The post-step
# save only runs when the job succeeded, so a failed generation is never saved.
# Extend the extension list if a component ships extensionless headers.
- name: Cache clang-tidy idedata (write on dev)
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && steps.key.outputs.skip != 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.key.outputs.path }}
key: ${{ steps.key.outputs.key }}
- name: Cache clang-tidy idedata (restore-only off dev)
if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') && steps.key.outputs.skip != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.key.outputs.path }}
key: ${{ steps.key.outputs.key }}
+21 -5
View File
@@ -26,6 +26,9 @@ runs:
# The native-IDF version is pinned in code, not in any file that feeds the
# other cache keys, so resolve it explicitly. Keying on it means the cache
# invalidates on a version bump (actions/cache never overwrites a key).
# Also key on the Python version: the cached IDF venv links to the
# runner's toolcache interpreter and is reinstalled every run after a
# runner image bump.
id: version
shell: bash
run: |
@@ -36,19 +39,32 @@ runs:
version=$(python -c 'from esphome.components.esp32 import ESP_IDF_FRAMEWORK_VERSION_LOOKUP as L; print(L["recommended"])')
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "python-version=$(python -c 'import platform; print(platform.python_version())')" >> "$GITHUB_OUTPUT"
# Mirror the adjacent PlatformIO cache: only dev-branch runs write the
# shared cache (so it lives in the default-branch scope readable by all
# PRs), and PRs are restore-only -- they never push multi-GB artifacts into
# their own scope / the repo quota (e.g. on a version-bump PR).
# their own scope / the repo quota (e.g. on a version-bump PR). The
# ci-cache-write label lets a PR write into its own scope to test the hit path;
# that costs about 1GB of the repo cache quota per run, so remove it when done.
# -slim: bump when prune-esp-idf changes what it removes; a key is never overwritten.
- name: Cache ESP-IDF install (write on dev)
if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && inputs.restore-only != 'true'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim
- name: Cache ESP-IDF install (restore-only off dev)
if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') || inputs.restore-only == 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim
# Install explicitly so the prune below sees the toolchains on a cache miss
# too, instead of the install happening inside the first build step.
- name: Install ESP-IDF
shell: bash
run: |
. venv/bin/activate
python -c 'from esphome.espidf.framework import check_esp_idf_install; check_esp_idf_install("${{ steps.version.outputs.version }}")'
- name: Prune ESP-IDF install
uses: ./.github/actions/prune-esp-idf
+34
View File
@@ -0,0 +1,34 @@
name: Prune ESP-IDF install
description: >
Remove the picolibc sysroots (1.1GB of the 3.9GB install) from the native
ESP-IDF toolchains; IDF 5.x links newlib. Skipped when an IDF 6 install is
present, which links picolibc (see esp32/__init__.py).
runs:
using: composite
steps:
- name: Prune picolibc
shell: bash
run: |
shopt -s nullglob
prefix="${ESPHOME_ESP_IDF_PREFIX:-$HOME/.esphome-idf}"
prefix="${prefix/#\~/$HOME}"
for fw in "$prefix"/frameworks/*/; do
case "$(basename "$fw")" in
[6-9].*) echo "IDF $(basename "$fw") installed, keeping picolibc"; exit 0 ;;
esac
done
n=0
for dir in "$prefix"/tools/*-esp-elf/*/*-esp-elf/picolibc; do
echo "Removing $dir ($(du -sh "$dir" | cut -f1))"
rm -rf "$dir"
n=$((n + 1))
done
# The marker rides along in the cache entry so a restored slim tree stays quiet.
if [ "$n" -gt 0 ]; then
touch "$prefix/.picolibc-pruned"
elif [ -d "$prefix/tools" ] && [ ! -f "$prefix/.picolibc-pruned" ]; then
echo "::warning::no picolibc sysroots matched under $prefix/tools"
fi
if [ -d "$prefix" ]; then
du -sh "$prefix"
fi
+64
View File
@@ -355,6 +355,15 @@ jobs:
fail-fast: false
matrix:
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
env:
# What the cache steps persist; libdeps is excluded (keyed per xdist
# worker and env, it never crosses runs).
INTEGRATION_PIO_CACHE_PATH: |
~/.esphome-integration-tests/platformio/platforms
~/.esphome-integration-tests/platformio/packages
~/.esphome-integration-tests/platformio/appstate.json
~/.esphome-integration-tests/platformio/.cache
~/.esphome-integration-tests/platformio/.esphome.pio.stamp.json
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -373,6 +382,14 @@ jobs:
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Restore integration PlatformIO cache
# Native platform + toolchain installed by shared_platformio_cache in
# tests/integration/conftest.py; a miss self-heals, so no restore-keys.
id: pio-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: integration-pio-v1-${{ runner.os }}-py${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'tests/integration/fixtures/cache_init.yaml', 'esphome/components/host/__init__.py') }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -416,6 +433,13 @@ jobs:
# esphome stores the PlatformIO ccache under the machine-global cache
# dir (see _ccache_env() in esphome/platformio/toolchain.py).
run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s
- name: Save integration PlatformIO cache
# Bucket 0 only; the others would race the same immutable key.
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.pio-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: ${{ steps.pio-cache.outputs.cache-primary-key }}
import-time:
name: Check import esphome.__main__ time
@@ -649,6 +673,12 @@ jobs:
with:
framework: arduino
- name: Cache clang-tidy idedata
if: matrix.cache_idf
uses: ./.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-arduino-tidy
- name: Cache nRF Connect SDK install
if: matrix.cache_sdk_nrf
uses: ./.github/actions/cache-sdk-nrf
@@ -698,6 +728,10 @@ jobs:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Prune ESP-IDF install before cache save
if: matrix.cache_idf && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }}
# yamllint disable-line rule:line-length
@@ -730,6 +764,11 @@ jobs:
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-idf-tidy
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
@@ -764,6 +803,10 @@ jobs:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
@@ -809,6 +852,11 @@ jobs:
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-idf-tidy
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
@@ -843,6 +891,10 @@ jobs:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
@@ -866,16 +918,19 @@ jobs:
name: Run script/clang-tidy for ESP32 S3
# yamllint disable-line rule:line-length
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
tidy_environment: esp32s3-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 P4
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
# so their code paths differ -- lint them under the P4 build too.
# yamllint disable-line rule:line-length
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
tidy_environment: esp32p4-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 C6
# yamllint disable-line rule:line-length
options: --environment esp32c6-idf-tidy --grep SOC_LP_I2C_SUPPORTED --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
tidy_environment: esp32c6-idf-tidy
steps:
- name: Check out code from GitHub
@@ -893,6 +948,11 @@ jobs:
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
with:
environment: ${{ matrix.tidy_environment }}
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
@@ -926,6 +986,10 @@ jobs:
script/clang-tidy --fix --changed ${{ matrix.options }}
fi
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
+1
View File
@@ -350,6 +350,7 @@ esphome/components/mipi_spi/* @clydebarrow
esphome/components/mitsubishi/* @RubyBailey
esphome/components/mitsubishi_cn105/* @crnjan
esphome/components/mixer/speaker/* @kahrendt
esphome/components/mk2pvrouter/* @FredM67
esphome/components/mlx90393/* @functionpointer
esphome/components/mlx90614/* @jesserockz
esphome/components/mmc5603/* @benhoff
+16 -17
View File
@@ -1670,20 +1670,26 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
if exit_code != 0:
return exit_code
if CORE.is_host:
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
program_path = str(toolchain.get_elf_path())
else:
from esphome.platformio.toolchain import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Successfully compiled program to path '%s'", program_path)
_LOGGER.info(
"Successfully compiled program to path '%s'", _host_program_path(config)
)
else:
_LOGGER.info("Successfully compiled program.")
return 0
def _host_program_path(config: ConfigType) -> str:
"""Return the compiled host ELF path."""
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
return str(toolchain.get_elf_path())
from esphome.platformio.toolchain import get_idedata
# Memoized by compile_program's own call; this is a dict lookup
return str(get_idedata(config).firmware_elf_path)
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
# Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
@@ -1728,14 +1734,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
return exit_code
_LOGGER.info("Successfully compiled program.")
if CORE.is_host:
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
program_path = str(toolchain.get_elf_path())
else:
from esphome.platformio.toolchain import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
program_path = _host_program_path(config)
_LOGGER.info("Running program from path '%s'", program_path)
return run_external_process(program_path)
+1
View File
@@ -78,6 +78,7 @@ from esphome.cpp_types import ( # noqa: F401
StringRef,
arduino_json_ns,
bool_,
char,
const_char_ptr,
double,
esphome_ns,
+23
View File
@@ -13,6 +13,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
get_esp32_variant,
)
import esphome.config_validation as cv
@@ -156,6 +157,17 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc_channel_t.ADC_CHANNEL_8,
10: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h
VARIANT_ESP32S31: {
42: adc_channel_t.ADC_CHANNEL_0,
43: adc_channel_t.ADC_CHANNEL_1,
44: adc_channel_t.ADC_CHANNEL_2,
45: adc_channel_t.ADC_CHANNEL_3,
46: adc_channel_t.ADC_CHANNEL_4,
47: adc_channel_t.ADC_CHANNEL_5,
48: adc_channel_t.ADC_CHANNEL_6,
49: adc_channel_t.ADC_CHANNEL_7,
},
}
# pin to adc2 channel mapping
@@ -225,6 +237,17 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc_channel_t.ADC_CHANNEL_8,
20: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h
VARIANT_ESP32S31: {
50: adc_channel_t.ADC_CHANNEL_0,
51: adc_channel_t.ADC_CHANNEL_1,
52: adc_channel_t.ADC_CHANNEL_2,
53: adc_channel_t.ADC_CHANNEL_3,
54: adc_channel_t.ADC_CHANNEL_4,
55: adc_channel_t.ADC_CHANNEL_5,
56: adc_channel_t.ADC_CHANNEL_6,
57: adc_channel_t.ADC_CHANNEL_7,
},
}
+40 -38
View File
@@ -74,9 +74,7 @@ void ADCSensor::setup() {
if (this->calibration_handle_ == nullptr) {
adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
// RISC-V variants (except C2) and S3 use curve fitting calibration
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
@@ -94,7 +92,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = this->attenuation_,
@@ -112,7 +110,11 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
#else // No calibration scheme available
(void) handle;
ESP_LOGD(TAG, "No calibration scheme for this variant, readings are uncalibrated");
this->setup_flags_.calibration_complete = false;
#endif
}
this->setup_flags_.init_complete = true;
@@ -121,23 +123,28 @@ void ADCSensor::setup() {
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
ESP_LOGCONFIG(
TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s\n"
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) || defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
const char *calibration_status = this->setup_flags_.calibration_complete ? "OK" : "FAILED";
#else
const char *calibration_status = "N/A"; // This variant has no calibration scheme
#endif
ESP_LOGCONFIG(TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s\n"
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
this->setup_flags_.handle_init_complete ? "OK" : "FAILED",
this->setup_flags_.config_complete ? "OK" : "FAILED", calibration_status,
this->setup_flags_.init_complete ? "OK" : "FAILED");
LOG_UPDATE_INTERVAL(this);
}
@@ -184,12 +191,11 @@ float ADCSensor::sample_fixed_attenuation_() {
} else {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else // Other ESP32 variants use line fitting calibration
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
#endif
this->calibration_handle_ = nullptr;
}
}
@@ -217,10 +223,9 @@ float ADCSensor::sample_autorange_() {
// Need to recalibrate for the new attenuation
if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif
this->calibration_handle_ = nullptr;
@@ -229,8 +234,7 @@ float ADCSensor::sample_autorange_() {
// Create new calibration handle for this attenuation
adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
@@ -242,7 +246,7 @@ float ADCSensor::sample_autorange_() {
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten,
(err == ESP_OK) ? "SUCCESS" : "FAILED", err);
#else
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = atten,
@@ -264,10 +268,9 @@ float ADCSensor::sample_autorange_() {
if (err != ESP_OK) {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
adc_cali_delete_scheme_curve_fitting(handle);
#else
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
adc_cali_delete_scheme_line_fitting(handle);
#endif
}
@@ -286,10 +289,9 @@ float ADCSensor::sample_autorange_() {
ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage);
}
// Clean up calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
adc_cali_delete_scheme_curve_fitting(handle);
#else
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
adc_cali_delete_scheme_line_fitting(handle);
#endif
} else {
+9
View File
@@ -3,6 +3,7 @@ import logging
import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import (
VARIANT_ESP32S31,
get_esp32_variant,
include_builtin_idf_component,
require_adc_oneshot_iram,
@@ -56,6 +57,14 @@ def validate_config(config: ConfigType) -> ConfigType:
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
# The S31 ADC supports a single attenuation level (SOC_ADC_ATTEN_NUM is 1)
if (
CORE.is_esp32
and get_esp32_variant() == VARIANT_ESP32S31
and config.get(CONF_ATTENUATION, "0db") != "0db"
):
raise cv.Invalid("ESP32-S31 only supports 'attenuation: 0db'")
if config.get(CONF_ATTENUATION, None) == "auto" and config.get(CONF_SAMPLES, 1) > 1:
raise cv.Invalid(
"Automatic attenuation cannot be used when multisampling is set"
+125 -13
View File
@@ -5,6 +5,7 @@ from typing import Any
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.const import CONF_DESCRIPTION
from esphome.components.logger import request_log_listener
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
@@ -41,10 +42,12 @@ from esphome.const import (
CONF_TAG,
CONF_THEN,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_VARIABLES,
)
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.helpers import fnv1_hash
from esphome.types import ConfigFragmentType, ConfigType
# Compat alias: downstream consumers (e.g. device-builder) referenced the
@@ -125,6 +128,7 @@ SERVICE_ARG_FALLBACK_TYPES: dict[str, MockObj] = {
}
CONF_BATCH_DELAY = "batch_delay"
CONF_CUSTOM_SERVICES = "custom_services"
CONF_EXAMPLE = "example"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
@@ -228,14 +232,30 @@ def _validate_supports_response(value: Any) -> str:
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small
ESP8266_ACTION_STRINGS_MAX_TOTAL = 384
VARIABLE_SCHEMA = cv.Schema(
{
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
cv.Optional(CONF_EXAMPLE): cv.string_strict,
}
)
# Accepts the plain `name: type` shorthand or the full mapping form
validate_variable = cv.maybe_simple_value(VARIABLE_SCHEMA, key=CONF_TYPE)
ACTIONS_SCHEMA = automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
cv.validate_id_name: validate_variable,
}
),
# No default - auto-detected by _auto_detect_supports_response
@@ -352,6 +372,85 @@ CONFIG_SCHEMA = cv.All(
)
def _has_action_metadata(actions: list[ConfigType]) -> bool:
# Empty strings count as unset, matching _action_strings
return any(
conf.get(CONF_DESCRIPTION)
or any(
var_.get(CONF_DESCRIPTION) or var_.get(CONF_EXAMPLE)
for var_ in conf[CONF_VARIABLES].values()
)
for conf in actions
)
def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
"""Strings of one action in the table order UserServiceStatic (user_services.h) expects."""
# An empty description or example is treated as unset
strings: list[str | None] = [conf[CONF_ACTION]]
if has_metadata:
strings.append(conf.get(CONF_DESCRIPTION) or None)
for name, var_ in conf[CONF_VARIABLES].items():
strings.append(name)
if has_metadata:
strings += [
var_.get(CONF_DESCRIPTION) or None,
var_.get(CONF_EXAMPLE) or None,
]
return strings
def _action_strings_size(strings: list[str | None]) -> int:
"""Bytes needed to copy every string out of flash, each with its terminator."""
return sum(
len(string.encode("utf-8")) + 1 for string in strings if string is not None
)
def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
if not CORE.is_esp8266:
return config
actions = config.get(CONF_ACTIONS, [])
has_metadata = _has_action_metadata(actions)
for conf in actions:
size = _action_strings_size(_action_strings(conf, has_metadata))
if size > ESP8266_ACTION_STRINGS_MAX_TOTAL:
raise cv.Invalid(
f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, "
f"description and example text; ESP8266 allows at most "
f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action"
)
return config
FINAL_VALIDATE_SCHEMA = _validate_esp8266_action_strings
def _add_action_strings(
index: int, strings: list[str | None], interned: dict[str, MockObj]
) -> MockObj:
"""Emit the PROGMEM string table for one action.
Each string is its own PROGMEM array because on ESP8266 .rodata is RAM, and identical
strings are shared between actions through `interned`.
"""
entries: list[MockObj] = []
for string in strings:
if string is None:
entries.append(cg.nullptr)
continue
if (var := interned.get(string)) is None:
var = interned[string] = cg.progmem_array(
ID(f"api_action_str{len(interned)}", is_declaration=True, type=cg.char),
string,
)
entries.append(var)
return cg.progmem_array(
ID(f"api_action{index}_strings", is_declaration=True, type=cg.const_char_ptr),
entries,
)
@coroutine_with_priority(CoroPriority.WEB)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -371,8 +470,10 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
actions = config.get(CONF_ACTIONS, [])
has_user_actions = bool(actions) or config[CONF_CUSTOM_SERVICES]
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
if has_user_actions:
cg.add_define("USE_API_USER_DEFINED_ACTIONS")
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
@@ -385,10 +486,17 @@ async def to_code(config: ConfigType) -> None:
if config[CONF_HOMEASSISTANT_STATES]:
cg.add_define("USE_API_HOMEASSISTANT_STATES")
if actions := config.get(CONF_ACTIONS, []):
scratch_size = 0
if actions:
# Metadata is compiled in for every action once any action declares it, because the
# string table layout is fixed by the define rather than per action
has_metadata = _has_action_metadata(actions)
if has_metadata:
cg.add_define("USE_API_USER_DEFINED_ACTION_METADATA")
interned_strings: dict[str, MockObj] = {}
# Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.MockObj] = []
for conf in actions:
for index, conf in enumerate(actions):
func_args: list[tuple[MockObj, str]] = []
service_template_args: list[MockObj] = [] # User service argument types
@@ -421,22 +529,23 @@ async def to_code(config: ConfigType) -> None:
conf.get(CONF_THEN, [])
)
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items():
if has_non_synchronous and var_ in SERVICE_ARG_FALLBACK_TYPES:
native = SERVICE_ARG_FALLBACK_TYPES[var_]
var_type = var_[CONF_TYPE]
if has_non_synchronous and var_type in SERVICE_ARG_FALLBACK_TYPES:
native = SERVICE_ARG_FALLBACK_TYPES[var_type]
else:
native = SERVICE_ARG_NATIVE_TYPES[var_]
native = SERVICE_ARG_NATIVE_TYPES[var_type]
service_template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)
strings = _action_strings(conf, has_metadata)
table = _add_action_strings(index, strings, interned_strings)
if CORE.is_esp8266:
scratch_size = max(scratch_size, _action_strings_size(strings))
# Template args: supports_response mode, then user service arg types
templ = cg.TemplateArguments(supports_response, *service_template_args)
# Key is hashed here because the name is not readable at runtime on ESP8266
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID],
templ,
conf[CONF_ACTION],
service_arg_names,
conf[CONF_TRIGGER_ID], templ, table, fnv1_hash(conf[CONF_ACTION])
)
triggers.append(trigger)
auto = await automation.build_automation(trigger, func_args, conf)
@@ -458,6 +567,9 @@ async def to_code(config: ConfigType) -> None:
cg.add(auto.add_actions([unregister_action]))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CORE.is_esp8266 and has_user_actions:
# Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action
cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1))
if CONF_ON_CLIENT_CONNECTED in config:
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
+3
View File
@@ -1034,6 +1034,8 @@ message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
ServiceArgType type = 2;
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ListEntitiesServicesResponse {
option (id) = 41;
@@ -1044,6 +1046,7 @@ message ListEntitiesServicesResponse {
fixed32 key = 2 [(force) = true];
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
+18
View File
@@ -1275,12 +1275,24 @@ uint8_t *ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer PROTO_ENC
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->example);
#endif
return pos;
}
uint32_t ListEntitiesServicesArgument::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_length(1, this->name.size());
size += this->type ? 2 : 0;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->description.size());
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->example.size());
#endif
return size;
}
uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1291,6 +1303,9 @@ uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENC
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it);
}
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->description);
#endif
return pos;
}
uint32_t ListEntitiesServicesResponse::calculate_size() const {
@@ -1303,6 +1318,9 @@ uint32_t ListEntitiesServicesResponse::calculate_size() const {
}
}
size += this->supports_response ? 2 : 0;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->description.size());
#endif
return size;
}
bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) {
+10 -1
View File
@@ -1317,6 +1317,12 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
public:
StringRef name{};
enums::ServiceArgType type{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef example{};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1328,7 +1334,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
class ListEntitiesServicesResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 41;
static constexpr uint8_t ESTIMATED_SIZE = 50;
static constexpr uint8_t ESTIMATED_SIZE = 59;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
#endif
@@ -1336,6 +1342,9 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
+9
View File
@@ -1500,6 +1500,12 @@ const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument"));
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("example"), this->example);
#endif
return out.c_str();
}
const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
@@ -1512,6 +1518,9 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
out.append("\n");
}
dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
return out.c_str();
}
const char *ExecuteServiceArgument::dump_to(DumpBuffer &out) const {
+2 -1
View File
@@ -99,7 +99,8 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response();
UserActionScratch scratch;
auto resp = service->encode_list_service_response(scratch);
if (!this->client_->send_message(resp))
return false;
// at_ is this service's index
+43
View File
@@ -1,9 +1,52 @@
#include "user_services.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
StringRef UserServiceStatic::str_(size_t idx, std::span<char> &scratch) const {
const char *s = progmem_read_ptr(&this->strings_[idx]);
if (s == nullptr)
return {};
#ifdef USE_ESP8266
// Codegen sizes the scratch buffer for the largest service; the bound only guards other callers
if (scratch.empty())
return {};
size_t len = strnlen_P(s, scratch.size() - 1);
progmem_memcpy(scratch.data(), s, len);
scratch[len] = '\0';
StringRef ref(scratch.data(), len);
scratch = scratch.subspan(len + 1);
return ref;
#else
return StringRef(s);
#endif
}
ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
std::span<const enums::ServiceArgType> arg_types, std::span<char> scratch) const {
ListEntitiesServicesResponse msg;
msg.name = this->str_(0, scratch);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
msg.description = this->str_(1, scratch);
#endif
msg.args.init(arg_types.size());
for (size_t i = 0; i < arg_types.size(); i++) {
size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS;
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = this->str_(base, scratch);
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
arg.description = this->str_(base + 1, scratch);
arg.example = this->str_(base + 2, scratch);
#endif
}
return msg;
}
template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; }
template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &arg) {
if (arg.legacy_int != 0)
+56 -37
View File
@@ -1,5 +1,6 @@
#pragma once
#include <span>
#include <tuple>
#include <utility>
#include <vector>
@@ -19,7 +20,9 @@ class APIServer;
class UserServiceDescriptor {
public:
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
/// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into
/// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it.
virtual ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -34,29 +37,51 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
template<typename T> enums::ServiceArgType to_service_arg_type();
// Base class for YAML-defined services (most common case)
// Stores only pointers to string literals in flash - no heap allocation
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
public:
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names,
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
: name_(name), arg_names_(arg_names), supports_response_(supports_response) {
this->key_ = fnv1_hash(name);
}
// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
#ifdef USE_ESP8266
using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
#else
using UserActionScratch = std::array<char, 0>;
#endif
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
// each argument name; with USE_API_USER_DEFINED_ACTION_METADATA the action description follows
// the name and every argument is (name, description, example). Unset metadata is nullptr.
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
static constexpr size_t USER_ACTION_HEADER_STRINGS = 2;
static constexpr size_t USER_ACTION_ARG_STRINGS = 3;
#else
static constexpr size_t USER_ACTION_HEADER_STRINGS = 1;
static constexpr size_t USER_ACTION_ARG_STRINGS = 1;
#endif
class UserServiceStatic : public UserServiceDescriptor {
public:
UserServiceStatic(const char *const *strings, uint32_t key,
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
: strings_(strings), key_(key), supports_response_(supports_response) {}
protected:
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
std::span<char> scratch) const;
/// Reference table entry `idx`; nullptr gives an empty StringRef.
/// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span
/// is advanced past the copy.
StringRef str_(size_t idx, std::span<char> &scratch) const;
const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
uint32_t key_;
enums::SupportsResponseType supports_response_;
};
template<typename... Ts> class UserServiceBase : public UserServiceStatic {
public:
using UserServiceStatic::UserServiceStatic;
ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) override {
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = StringRef(this->arg_names_[i]);
}
return msg;
return this->encode_list_service_response_(arg_types, scratch);
}
bool execute_service(const ExecuteServiceRequest &req) override {
@@ -89,12 +114,6 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
}
// Pointers to string literals in flash - no heap allocation
const char *name_;
std::array<const char *, sizeof...(Ts)> arg_names_;
uint32_t key_{0};
enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE};
};
// Separate class for custom_api_device services (rare case)
@@ -106,7 +125,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
this->key_ = fnv1_hash(this->name_.c_str());
}
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.key = this->key_;
@@ -167,8 +186,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<Ts...> {
public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {}
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_NONE) {}
protected:
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
@@ -179,8 +198,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, bool, Ts...> {
public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
protected:
void execute(uint32_t call_id, bool return_response, Ts... x) override {
@@ -193,8 +212,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {}
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_ONLY) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
@@ -205,8 +224,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {}
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_STATUS) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
+4 -2
View File
@@ -23,8 +23,10 @@ void AQISensor::setup() {
void AQISensor::dump_config() {
ESP_LOGCONFIG(TAG, "AQI Sensor:");
ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI");
ESP_LOGCONFIG(TAG, " Extended Range: %s", this->extended_range_ ? "enabled" : "disabled");
ESP_LOGCONFIG(TAG, " Calculation Type: %s",
this->aqi_calc_type_ == AQI_TYPE ? LOG_STR_LITERAL("AQI") : LOG_STR_LITERAL("CAQI"));
ESP_LOGCONFIG(TAG, " Extended Range: %s",
this->extended_range_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
if (this->pm_2_5_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str());
}
@@ -100,13 +100,15 @@ void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) {
}
this->is_valid_ = false;
this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
ESP_LOGV(TAG, "Multi Click: You can now %s the button.",
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
this->is_valid_ = true;
});
}
void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.",
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
this->is_valid_ = false;
this->schedule_cooldown_();
});
@@ -162,8 +162,9 @@ void BME680BSECComponent::dump_config() {
" Supply Voltage: %sV\n"
" Sample Rate: %s\n"
" State Save Interval: %" PRIu32 "ms",
this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile",
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8",
this->temperature_offset_,
this->iaq_mode_ == IAQ_MODE_STATIC ? LOG_STR_LITERAL("Static") : LOG_STR_LITERAL("Mobile"),
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? LOG_STR_LITERAL("3.3") : LOG_STR_LITERAL("1.8"),
BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
+1
View File
@@ -16,6 +16,7 @@ CONF_CO2_EQUIVALENT = "co2_equivalent"
CONF_COLOR_DEPTH = "color_depth"
CONF_CRC_ENABLE = "crc_enable"
CONF_DATA_BITS = "data_bits"
CONF_DESCRIPTION = "description"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
+8 -5
View File
@@ -249,7 +249,7 @@ bool CS5460AComponent::check_status_() {
bool dir = status & (1 << 21);
if (current_gain_ < 0)
dir = !dir;
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? "negative" : "positive");
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? LOG_STR_LITERAL("negative") : LOG_STR_LITERAL("positive"));
clear |= 1 << 22;
}
@@ -319,7 +319,9 @@ void CS5460AComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"CS5460A:\n"
" Init status: %s",
state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other"));
state == COMPONENT_STATE_LOOP
? LOG_STR_LITERAL("OK")
: (state == COMPONENT_STATE_FAILED ? LOG_STR_LITERAL("failed") : LOG_STR_LITERAL("other")));
LOG_PIN(" CS Pin: ", cs_);
ESP_LOGCONFIG(TAG,
" Samples / cycle: %" PRIu32 "\n"
@@ -330,9 +332,10 @@ void CS5460AComponent::dump_config() {
" Current HPF: %s\n"
" Voltage HPF: %s\n"
" Pulse energy: %.2f Wh",
samples_, phase_offset_, pga_gain_ == CS5460A_PGA_GAIN_50X ? "50x" : "10x", current_gain_,
voltage_gain_, current_hpf_ ? "enabled" : "disabled", voltage_hpf_ ? "enabled" : "disabled",
pulse_energy_wh_);
samples_, phase_offset_,
pga_gain_ == CS5460A_PGA_GAIN_50X ? LOG_STR_LITERAL("50x") : LOG_STR_LITERAL("10x"), current_gain_,
voltage_gain_, current_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"),
voltage_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"), pulse_energy_wh_);
LOG_SENSOR(" ", "Voltage", voltage_sensor_);
LOG_SENSOR(" ", "Current", current_sensor_);
LOG_SENSOR(" ", "Power", power_sensor_);
+2 -2
View File
@@ -20,8 +20,8 @@ void DHT::dump_config() {
"DHT:\n"
" %sModel: %s\n"
" Internal pull-up: %s",
this->is_auto_detect_ ? "Auto-detected " : "",
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent",
this->is_auto_detect_ ? LOG_STR_LITERAL("Auto-detected ") : "",
this->model_ == DHT_MODEL_DHT11 ? LOG_STR_LITERAL("DHT11") : LOG_STR_LITERAL("DHT22 or equivalent"),
ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_PIN(" Pin: ", this->t_pin_);
LOG_UPDATE_INTERVAL(this);
+1 -1
View File
@@ -93,7 +93,7 @@ void Emc2101Component::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? "DAC" : "PWM");
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? LOG_STR_LITERAL("DAC") : LOG_STR_LITERAL("PWM"));
if (this->dac_mode_) {
ESP_LOGCONFIG(TAG, " DAC Conversion Rate: %X", this->dac_conversion_rate_);
} else {
+1 -1
View File
@@ -216,7 +216,7 @@ void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status)
// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems.
bool ENS210Component::set_low_power_(bool enable) {
uint8_t low_power_cmd = enable ? 0x01 : 0x00;
ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false");
ESP_LOGD(TAG, "Enable low power: %s", enable ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd);
delay(ENS210_BOOTING_MS);
return result;
+8 -2
View File
@@ -246,7 +246,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_https_server", # HTTPS server - ESPHome has its own web server
"esp_lcd", # LCD controller drivers - only needed by display component
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
"esp_phy", # RF PHY - esp_wifi/bt/ieee802154 pull it back when they are in the build
"esp_phy", # RF PHY - re-included by internal_temperature on the original ESP32; esp_wifi/bt/ieee802154 pull it back
"esp_wifi", # WiFi stack - re-included by request_wifi(), espnow; bt pulls it back for BLE builds
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
@@ -3249,7 +3249,13 @@ def _write_sdkconfig():
if write_file_if_changed(internal_path, contents):
# internal changed, update real one
write_file_if_changed(sdk_path, contents)
clean_build(clear_pio_cache=False)
if not CORE.using_toolchain_esp_idf:
# PIO's dependency tracking under-declares sdkconfig inputs
# (ldgen, linker scripts); without a clean the image can be
# unbootable (esphome#15336). The esp-idf toolchain tracks
# sdkconfig via IDF's cmake and has_outdated_files(), so a
# reconfigure suffices there; everything else fails safe.
clean_build(clear_pio_cache=False)
def _write_idf_component_yml():
+13 -4
View File
@@ -5,11 +5,14 @@ import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
# Per the ESP32-S31 datasheet (page 96):
# Per the ESP32-S31 IDF DOCS and datasheet:
# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
_ESP32S31_SPI_FLASH_PINS: set[int] = {27, 28, 29, 31, 32, 33}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source.
_ESP32S31_STRAPPING_PINS: set[int] = {37, 60, 61}
_ESP32S31_SPI_FLASH_PINS: set[int] = {26, 27, 28, 30, 31, 32}
_ESP32S31_INVALID_PINS: set[int] = {29, 41}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source;
# GPIO36 sets the VDD_SPI voltage.
_ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61}
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
@@ -19,6 +22,8 @@ _LOGGER = logging.getLogger(__name__)
def esp32_s31_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 61:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)")
if value in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.")
if value in _ESP32S31_SPI_FLASH_PINS:
raise cv.Invalid(
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
@@ -33,6 +38,10 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
if num < 0 or num > 61:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)")
# Checked here as well so ignore_pin_validation_error cannot bypass it;
# these pins are not bonded and can never work
if num in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{num} does not exist on ESP32-S31.")
if is_input:
# All ESP32 pins support input mode
pass
@@ -128,7 +128,8 @@ void ESPHomeOTAComponent::dump_config() {
esp_partition_iterator_release(it);
esp_bootloader_desc_t bootloader_desc;
esp_err_t err = esp_ota_get_bootloader_description(nullptr, &bootloader_desc);
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s", (err == ESP_OK) ? bootloader_desc.idf_ver : "version unknown");
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s",
(err == ESP_OK) ? bootloader_desc.idf_ver : LOG_STR_LITERAL("version unknown"));
#endif // USE_ESP32
#endif // USE_OTA_PARTITIONS
}
+10 -7
View File
@@ -93,7 +93,8 @@ void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback)
// setup callbacks to react to sensor changes
open_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) {
this->endstop_reached_(true);
@@ -106,7 +107,8 @@ void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback
this->close_feedback_ = close_feedback;
close_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
this->endstop_reached_(false);
@@ -144,7 +146,8 @@ void FeedbackCover::endstop_reached_(bool open_endstop) {
// from a position slightly past the endpoint
if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) {
float dur = (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur);
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
open_endstop ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
// if there is no external mechanism, stop the cover
if (!this->has_built_in_endstop_) {
@@ -366,7 +369,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
// the case when an obstacle appears while moving is handled in the callback
if (obstacle != nullptr && obstacle->state) {
ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? "Open" : "Close");
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"));
return;
}
#endif
@@ -383,9 +386,9 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
this->set_current_operation_(dir, true);
this->prev_command_trigger_ = trig;
ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? "OPEN"
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
: "STOP");
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
: dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
: LOG_STR_LITERAL("STOP"));
trig->trigger();
}
}
@@ -547,8 +547,8 @@ void FingerprintGrowComponent::dump_config() {
" System Identifier Code: 0x%.4X\n"
" Touch Sensing Pin: %s\n"
" Sensor Power Pin: %s",
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
this->has_power_pin_ ? power_pin_buf : "None");
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : LOG_STR_LITERAL("None"),
this->has_power_pin_ ? power_pin_buf : LOG_STR_LITERAL("None"));
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
} else {
@@ -35,18 +35,20 @@ void GraphicalDisplayMenu::setup() {
}
void GraphicalDisplayMenu::dump_config() {
ESP_LOGCONFIG(TAG,
"Graphical Display Menu\n"
" Has Display: %s\n"
" Popup Mode: %s\n"
" Advanced Drawing Mode: %s\n"
" Has Font: %s\n"
" Mode: %s\n"
" Active: %s\n"
" Menu items:",
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
YESNO(this->font_ != nullptr),
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_));
ESP_LOGCONFIG(
TAG,
"Graphical Display Menu\n"
" Has Display: %s\n"
" Popup Mode: %s\n"
" Advanced Drawing Mode: %s\n"
" Has Font: %s\n"
" Mode: %s\n"
" Active: %s\n"
" Menu items:",
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
YESNO(this->font_ != nullptr),
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? LOG_STR_LITERAL("Rotary") : LOG_STR_LITERAL("Joystick"),
YESNO(this->active_));
for (size_t i = 0; i < this->displayed_item_->items_size(); i++) {
auto *item = this->displayed_item_->get_item(i);
ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(),
@@ -1,105 +1,68 @@
#include "growatt_solar.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::growatt_solar {
namespace helpers = modbus::helpers;
static const char *const TAG = "growatt_solar";
static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion
void GrowattSolar::loop() {
// If update() was unable to send we retry until we can send.
if (!this->waiting_to_update_)
return;
update();
}
void GrowattSolar::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]); }
void GrowattSolar::update() {
// If our last send has had no reply yet, and it wasn't that long ago, do nothing.
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_send_ < this->get_update_interval() / 2) {
return;
}
// The bus might be slow, or there might be other devices, or other components might be talking to our device.
if (!this->ready_for_immediate_send()) {
this->waiting_to_update_ = true;
return;
}
this->waiting_to_update_ = false;
this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]);
this->last_send_ = millis();
}
void GrowattSolar::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
auto data = modbus::helpers::server_pdu_payload(response_pdu);
// Other components might be sending commands to our device. But we don't get called with enough
// context to know what is what. So if we didn't do a send, we ignore the data.
if (!this->last_send_)
return;
this->last_send_ = 0;
// Also ignore the data if the message is too short. Otherwise we will publish invalid values.
if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2)
void GrowattSolar::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) {
if (!modbus::succeeded(status))
return;
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void {
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
// correct for any read range, so the poll may be split into multiple requests.
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr)
return;
float value = encode_uint16(data[i * 2], data[i * 2 + 1]) * unit;
sensor->publish_state(value);
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
};
auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg1, size_t reg2, float unit) -> void {
float value = ((encode_uint16(data[reg1 * 2], data[reg1 * 2 + 1]) << 16) +
encode_uint16(data[reg2 * 2], data[reg2 * 2 + 1])) *
unit;
if (sensor != nullptr)
sensor->publish_state(value);
auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
};
switch (this->protocol_version_) {
case RTU: {
publish_1_reg_sensor_state(this->inverter_status_, RTU_INVERTER_STATUS, 1);
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, RTU_PV_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU_PV1_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU_PV1_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, RTU_PV1_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU_PV2_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU_PV2_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, RTU_PV2_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, RTU_GRID_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU_GRID_FREQUENCY, TWO_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU_PHASE1_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU_PHASE1_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER,
RTU_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU_PHASE2_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU_PHASE2_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER,
RTU_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU_PHASE3_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU_PHASE3_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER,
RTU_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, RTU_TODAY_PRODUCTION + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION,
RTU_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->inverter_module_temp_, RTU_INVERTER_MODULE_TEMP, ONE_DEC_UNIT);
break;
@@ -107,42 +70,33 @@ void GrowattSolar::on_response(std::span<const uint8_t> request_pdu, std::span<c
case RTU2: {
publish_1_reg_sensor_state(this->inverter_status_, RTU2_INVERTER_STATUS, 1);
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, RTU2_PV_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU2_PV1_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU2_PV1_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, RTU2_PV1_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU2_PV2_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU2_PV2_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, RTU2_PV2_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, RTU2_GRID_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU2_GRID_FREQUENCY, TWO_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU2_PHASE1_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU2_PHASE1_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER,
RTU2_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU2_PHASE2_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU2_PHASE2_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER,
RTU2_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU2_PHASE3_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU2_PHASE3_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER,
RTU2_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, RTU2_TODAY_PRODUCTION + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION,
RTU2_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->inverter_module_temp_, RTU2_INVERTER_MODULE_TEMP, ONE_DEC_UNIT);
break;
@@ -17,59 +17,59 @@ enum GrowattProtocolVersion {
};
// Register addresses for the RTU protocol.
constexpr size_t RTU_INVERTER_STATUS = 0; // length = 1
constexpr size_t RTU_PV_ACTIVE_POWER = 1; // length = 2
constexpr size_t RTU_PV1_VOLTAGE = 3; // length = 1
constexpr size_t RTU_PV1_CURRENT = 4; // length = 1
constexpr size_t RTU_PV1_ACTIVE_POWER = 5; // length = 2
constexpr size_t RTU_PV2_VOLTAGE = 7; // length = 1
constexpr size_t RTU_PV2_CURRENT = 8; // length = 1
constexpr size_t RTU_PV2_ACTIVE_POWER = 9; // length = 2
constexpr size_t RTU_GRID_ACTIVE_POWER = 11; // length = 2
constexpr size_t RTU_GRID_FREQUENCY = 13; // length = 1
constexpr size_t RTU_PHASE1_VOLTAGE = 14; // length = 1
constexpr size_t RTU_PHASE1_CURRENT = 15; // length = 1
constexpr size_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2
constexpr size_t RTU_PHASE2_VOLTAGE = 18; // length = 1
constexpr size_t RTU_PHASE2_CURRENT = 19; // length = 1
constexpr size_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2
constexpr size_t RTU_PHASE3_VOLTAGE = 22; // length = 1
constexpr size_t RTU_PHASE3_CURRENT = 23; // length = 1
constexpr size_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2
constexpr size_t RTU_TODAY_PRODUCTION = 26; // length = 2
constexpr size_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2
constexpr size_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1
constexpr uint16_t RTU_INVERTER_STATUS = 0; // length = 1
constexpr uint16_t RTU_PV_ACTIVE_POWER = 1; // length = 2
constexpr uint16_t RTU_PV1_VOLTAGE = 3; // length = 1
constexpr uint16_t RTU_PV1_CURRENT = 4; // length = 1
constexpr uint16_t RTU_PV1_ACTIVE_POWER = 5; // length = 2
constexpr uint16_t RTU_PV2_VOLTAGE = 7; // length = 1
constexpr uint16_t RTU_PV2_CURRENT = 8; // length = 1
constexpr uint16_t RTU_PV2_ACTIVE_POWER = 9; // length = 2
constexpr uint16_t RTU_GRID_ACTIVE_POWER = 11; // length = 2
constexpr uint16_t RTU_GRID_FREQUENCY = 13; // length = 1
constexpr uint16_t RTU_PHASE1_VOLTAGE = 14; // length = 1
constexpr uint16_t RTU_PHASE1_CURRENT = 15; // length = 1
constexpr uint16_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2
constexpr uint16_t RTU_PHASE2_VOLTAGE = 18; // length = 1
constexpr uint16_t RTU_PHASE2_CURRENT = 19; // length = 1
constexpr uint16_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2
constexpr uint16_t RTU_PHASE3_VOLTAGE = 22; // length = 1
constexpr uint16_t RTU_PHASE3_CURRENT = 23; // length = 1
constexpr uint16_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2
constexpr uint16_t RTU_TODAY_PRODUCTION = 26; // length = 2
constexpr uint16_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2
constexpr uint16_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1
// Input register addresses for the RTU2 protocol as described
// in the "GROWATT INVERTER MODBUS PROTOCOL_II V1.39" document.
constexpr size_t RTU2_INVERTER_STATUS = 0; // length = 1
constexpr size_t RTU2_PV_ACTIVE_POWER = 1; // length = 2
constexpr size_t RTU2_PV1_VOLTAGE = 3; // length = 1
constexpr size_t RTU2_PV1_CURRENT = 4; // length = 1
constexpr size_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2
constexpr size_t RTU2_PV2_VOLTAGE = 7; // length = 1
constexpr size_t RTU2_PV2_CURRENT = 8; // length = 1
constexpr size_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2
constexpr size_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2
constexpr size_t RTU2_GRID_FREQUENCY = 37; // length = 1
constexpr size_t RTU2_PHASE1_VOLTAGE = 38; // length = 1
constexpr size_t RTU2_PHASE1_CURRENT = 39; // length = 1
constexpr size_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2
constexpr size_t RTU2_PHASE2_VOLTAGE = 42; // length = 1
constexpr size_t RTU2_PHASE2_CURRENT = 43; // length = 1
constexpr size_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2
constexpr size_t RTU2_PHASE3_VOLTAGE = 46; // length = 1
constexpr size_t RTU2_PHASE3_CURRENT = 47; // length = 1
constexpr size_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2
constexpr size_t RTU2_TODAY_PRODUCTION = 53; // length = 2
constexpr size_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2
constexpr size_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1
constexpr uint16_t RTU2_INVERTER_STATUS = 0; // length = 1
constexpr uint16_t RTU2_PV_ACTIVE_POWER = 1; // length = 2
constexpr uint16_t RTU2_PV1_VOLTAGE = 3; // length = 1
constexpr uint16_t RTU2_PV1_CURRENT = 4; // length = 1
constexpr uint16_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2
constexpr uint16_t RTU2_PV2_VOLTAGE = 7; // length = 1
constexpr uint16_t RTU2_PV2_CURRENT = 8; // length = 1
constexpr uint16_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2
constexpr uint16_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2
constexpr uint16_t RTU2_GRID_FREQUENCY = 37; // length = 1
constexpr uint16_t RTU2_PHASE1_VOLTAGE = 38; // length = 1
constexpr uint16_t RTU2_PHASE1_CURRENT = 39; // length = 1
constexpr uint16_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2
constexpr uint16_t RTU2_PHASE2_VOLTAGE = 42; // length = 1
constexpr uint16_t RTU2_PHASE2_CURRENT = 43; // length = 1
constexpr uint16_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2
constexpr uint16_t RTU2_PHASE3_VOLTAGE = 46; // length = 1
constexpr uint16_t RTU2_PHASE3_CURRENT = 47; // length = 1
constexpr uint16_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2
constexpr uint16_t RTU2_TODAY_PRODUCTION = 53; // length = 2
constexpr uint16_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2
constexpr uint16_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1
class GrowattSolar final : public PollingComponent, public modbus::ModbusClientDevice {
public:
void loop() override;
void update() override;
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) override;
void dump_config() override;
void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; }
@@ -104,9 +104,6 @@ class GrowattSolar final : public PollingComponent, public modbus::ModbusClientD
}
protected:
bool waiting_to_update_{false};
uint32_t last_send_{0};
struct GrowattPhase {
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
@@ -69,10 +69,12 @@ void GT911Touchscreen::setup_internal_() {
// Direct MCU pin: attach a hardware interrupt, no polling needed.
this->attach_interrupt_(static_cast<InternalGPIOPin *>(this->interrupt_pin_),
active_high ? gpio::INTERRUPT_RISING_EDGE : gpio::INTERRUPT_FALLING_EDGE);
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s", active_high ? "HIGH" : "LOW");
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s",
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
} else {
// IO expander pin: leave as output for configuration only.
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", active_high ? "HIGH" : "LOW");
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s",
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
}
}
}
+2 -1
View File
@@ -248,7 +248,8 @@ void HaierClimateBase::setup() {
void HaierClimateBase::dump_config() {
LOG_CLIMATE("", "Haier Climate", this);
ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none");
ESP_LOGCONFIG(TAG, " Device communication status: %s",
this->valid_connection() ? LOG_STR_LITERAL("established") : LOG_STR_LITERAL("none"));
}
void HaierClimateBase::loop() {
+5 -5
View File
@@ -343,11 +343,11 @@ void HonClimate::dump_config() {
this->hvac_hardware_info_.value().software_version_,
this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_);
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
(this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""),
(this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""),
(this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""),
(this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""),
(this->hvac_hardware_info_.value().functions_[4] ? " role" : ""));
(this->hvac_hardware_info_.value().functions_[0] ? LOG_STR_LITERAL(" interactive") : ""),
(this->hvac_hardware_info_.value().functions_[1] ? LOG_STR_LITERAL(" controller-device") : ""),
(this->hvac_hardware_info_.value().functions_[2] ? LOG_STR_LITERAL(" crc") : ""),
(this->hvac_hardware_info_.value().functions_[3] ? LOG_STR_LITERAL(" multinode") : ""),
(this->hvac_hardware_info_.value().functions_[4] ? LOG_STR_LITERAL(" role") : ""));
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
}
}
@@ -1,124 +1,71 @@
#include "havells_solar.h"
#include "havells_solar_registers.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::havells_solar {
namespace helpers = modbus::helpers;
static const char *const TAG = "havells_solar";
static const uint8_t MODBUS_REGISTER_COUNT = 48; // 48 x 16-bit registers
void HavellsSolar::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
auto data = modbus::helpers::server_pdu_payload(response_pdu);
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
ESP_LOGW(TAG, "Invalid size for HavellsSolar!");
return;
}
void HavellsSolar::on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) {
if (!modbus::succeeded(status))
return; // the hub already logs exception responses
/* Usage: returns the float value of 1 register read by modbus
Arg1: Register address * number of bytes per register
Arg2: Multiplier for final register value
*/
auto havells_solar_get_2_registers = [&](size_t i, float unit) -> float {
uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]);
return temp * unit;
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
// correct for any read range, so the poll may be split into multiple requests.
auto publish_1_register = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
};
/* Usage: returns the float value of 2 registers read by modbus
Arg1: Register address * number of bytes per register
Arg2: Multiplier for final register value
*/
auto havells_solar_get_1_register = [&](size_t i, float unit) -> float {
uint16_t temp = encode_uint16(data[i], data[i + 1]);
return temp * unit;
auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
};
for (uint8_t i = 0; i < 3; i++) {
auto phase = this->phases_[i];
auto &phase = this->phases_[i];
if (!phase.setup)
continue;
float voltage = havells_solar_get_1_register(HAVELLS_PHASE_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT);
float current = havells_solar_get_1_register(HAVELLS_PHASE_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT);
if (phase.voltage_sensor_ != nullptr)
phase.voltage_sensor_->publish_state(voltage);
if (phase.current_sensor_ != nullptr)
phase.current_sensor_->publish_state(current);
publish_1_register(phase.voltage_sensor_, HAVELLS_PHASE_1_VOLTAGE + i * 2, ONE_DEC_UNIT);
publish_1_register(phase.current_sensor_, HAVELLS_PHASE_1_CURRENT + i * 2, TWO_DEC_UNIT);
}
for (uint8_t i = 0; i < 2; i++) {
auto pv = this->pvs_[i];
auto &pv = this->pvs_[i];
if (!pv.setup)
continue;
float voltage = havells_solar_get_1_register(HAVELLS_PV_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT);
float current = havells_solar_get_1_register(HAVELLS_PV_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT);
float active_power = havells_solar_get_1_register(HAVELLS_PV_1_POWER * 2 + (i * 2), MULTIPLY_TEN_UNIT);
float voltage_sampled_by_secondary_cpu =
havells_solar_get_1_register(HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU * 2 + (i * 2), ONE_DEC_UNIT);
float insulation_of_p_to_ground =
havells_solar_get_1_register(HAVELLS_PV1_INSULATION_OF_P_TO_GROUND * 2 + (i * 2), NO_DEC_UNIT);
if (pv.voltage_sensor_ != nullptr)
pv.voltage_sensor_->publish_state(voltage);
if (pv.current_sensor_ != nullptr)
pv.current_sensor_->publish_state(current);
if (pv.active_power_sensor_ != nullptr)
pv.active_power_sensor_->publish_state(active_power);
if (pv.voltage_sampled_by_secondary_cpu_sensor_ != nullptr)
pv.voltage_sampled_by_secondary_cpu_sensor_->publish_state(voltage_sampled_by_secondary_cpu);
if (pv.insulation_of_p_to_ground_sensor_ != nullptr)
pv.insulation_of_p_to_ground_sensor_->publish_state(insulation_of_p_to_ground);
publish_1_register(pv.voltage_sensor_, HAVELLS_PV_1_VOLTAGE + i * 2, ONE_DEC_UNIT);
publish_1_register(pv.current_sensor_, HAVELLS_PV_1_CURRENT + i * 2, TWO_DEC_UNIT);
publish_1_register(pv.active_power_sensor_, HAVELLS_PV_1_POWER + i, MULTIPLY_TEN_UNIT);
publish_1_register(pv.voltage_sampled_by_secondary_cpu_sensor_, HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU + i,
ONE_DEC_UNIT);
publish_1_register(pv.insulation_of_p_to_ground_sensor_, HAVELLS_PV1_INSULATION_OF_P_TO_GROUND + i, NO_DEC_UNIT);
}
float frequency = havells_solar_get_1_register(HAVELLS_GRID_FREQUENCY * 2, TWO_DEC_UNIT);
float active_power = havells_solar_get_1_register(HAVELLS_SYSTEM_ACTIVE_POWER * 2, MULTIPLY_TEN_UNIT);
float reactive_power = havells_solar_get_1_register(HAVELLS_SYSTEM_REACTIVE_POWER * 2, TWO_DEC_UNIT);
float today_production = havells_solar_get_1_register(HAVELLS_TODAY_PRODUCTION * 2, TWO_DEC_UNIT);
float total_energy_production = havells_solar_get_2_registers(HAVELLS_TOTAL_ENERGY_PRODUCTION * 2, NO_DEC_UNIT);
float total_generation_time = havells_solar_get_2_registers(HAVELLS_TOTAL_GENERATION_TIME * 2, NO_DEC_UNIT);
float today_generation_time = havells_solar_get_1_register(HAVELLS_TODAY_GENERATION_TIME * 2, NO_DEC_UNIT);
float inverter_module_temp = havells_solar_get_1_register(HAVELLS_INVERTER_MODULE_TEMP * 2, NO_DEC_UNIT);
float inverter_inner_temp = havells_solar_get_1_register(HAVELLS_INVERTER_INNER_TEMP * 2, NO_DEC_UNIT);
float inverter_bus_voltage = havells_solar_get_1_register(HAVELLS_INVERTER_BUS_VOLTAGE * 2, NO_DEC_UNIT);
float insulation_pv_n_to_ground = havells_solar_get_1_register(HAVELLS_INSULATION_OF_PV_N_TO_GROUND * 2, NO_DEC_UNIT);
float gfci_value = havells_solar_get_1_register(HAVELLS_GFCI_VALUE * 2, NO_DEC_UNIT);
float dci_of_r = havells_solar_get_1_register(HAVELLS_DCI_OF_R * 2, NO_DEC_UNIT);
float dci_of_s = havells_solar_get_1_register(HAVELLS_DCI_OF_S * 2, NO_DEC_UNIT);
float dci_of_t = havells_solar_get_1_register(HAVELLS_DCI_OF_T * 2, NO_DEC_UNIT);
if (this->frequency_sensor_ != nullptr)
this->frequency_sensor_->publish_state(frequency);
if (this->active_power_sensor_ != nullptr)
this->active_power_sensor_->publish_state(active_power);
if (this->reactive_power_sensor_ != nullptr)
this->reactive_power_sensor_->publish_state(reactive_power);
if (this->today_production_sensor_ != nullptr)
this->today_production_sensor_->publish_state(today_production);
if (this->total_energy_production_sensor_ != nullptr)
this->total_energy_production_sensor_->publish_state(total_energy_production);
if (this->total_generation_time_sensor_ != nullptr)
this->total_generation_time_sensor_->publish_state(total_generation_time);
if (this->today_generation_time_sensor_ != nullptr)
this->today_generation_time_sensor_->publish_state(today_generation_time);
if (this->inverter_module_temp_sensor_ != nullptr)
this->inverter_module_temp_sensor_->publish_state(inverter_module_temp);
if (this->inverter_inner_temp_sensor_ != nullptr)
this->inverter_inner_temp_sensor_->publish_state(inverter_inner_temp);
if (this->inverter_bus_voltage_sensor_ != nullptr)
this->inverter_bus_voltage_sensor_->publish_state(inverter_bus_voltage);
if (this->insulation_pv_n_to_ground_sensor_ != nullptr)
this->insulation_pv_n_to_ground_sensor_->publish_state(insulation_pv_n_to_ground);
if (this->gfci_value_sensor_ != nullptr)
this->gfci_value_sensor_->publish_state(gfci_value);
if (this->dci_of_r_sensor_ != nullptr)
this->dci_of_r_sensor_->publish_state(dci_of_r);
if (this->dci_of_s_sensor_ != nullptr)
this->dci_of_s_sensor_->publish_state(dci_of_s);
if (this->dci_of_t_sensor_ != nullptr)
this->dci_of_t_sensor_->publish_state(dci_of_t);
publish_1_register(this->frequency_sensor_, HAVELLS_GRID_FREQUENCY, TWO_DEC_UNIT);
publish_1_register(this->active_power_sensor_, HAVELLS_SYSTEM_ACTIVE_POWER, MULTIPLY_TEN_UNIT);
publish_1_register(this->reactive_power_sensor_, HAVELLS_SYSTEM_REACTIVE_POWER, TWO_DEC_UNIT);
publish_1_register(this->today_production_sensor_, HAVELLS_TODAY_PRODUCTION, TWO_DEC_UNIT);
publish_2_registers(this->total_energy_production_sensor_, HAVELLS_TOTAL_ENERGY_PRODUCTION, NO_DEC_UNIT);
publish_2_registers(this->total_generation_time_sensor_, HAVELLS_TOTAL_GENERATION_TIME, NO_DEC_UNIT);
publish_1_register(this->today_generation_time_sensor_, HAVELLS_TODAY_GENERATION_TIME, NO_DEC_UNIT);
publish_1_register(this->inverter_module_temp_sensor_, HAVELLS_INVERTER_MODULE_TEMP, NO_DEC_UNIT);
publish_1_register(this->inverter_inner_temp_sensor_, HAVELLS_INVERTER_INNER_TEMP, NO_DEC_UNIT);
publish_1_register(this->inverter_bus_voltage_sensor_, HAVELLS_INVERTER_BUS_VOLTAGE, NO_DEC_UNIT);
publish_1_register(this->insulation_pv_n_to_ground_sensor_, HAVELLS_INSULATION_OF_PV_N_TO_GROUND, NO_DEC_UNIT);
publish_1_register(this->gfci_value_sensor_, HAVELLS_GFCI_VALUE, NO_DEC_UNIT);
publish_1_register(this->dci_of_r_sensor_, HAVELLS_DCI_OF_R, NO_DEC_UNIT);
publish_1_register(this->dci_of_s_sensor_, HAVELLS_DCI_OF_S, NO_DEC_UNIT);
publish_1_register(this->dci_of_t_sensor_, HAVELLS_DCI_OF_T, NO_DEC_UNIT);
}
void HavellsSolar::update() { this->read_holding_registers(0, MODBUS_REGISTER_COUNT); }
@@ -77,7 +77,8 @@ class HavellsSolar final : public PollingComponent, public modbus::ModbusClientD
void update() override;
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) override;
void dump_config() override;
+1 -1
View File
@@ -38,7 +38,7 @@ void HDC302XComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"HDC302x:\n"
" Heater: %s",
this->heater_active_ ? "active" : "inactive");
this->heater_active_ ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("inactive"));
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temp_sensor_);
+4 -4
View File
@@ -59,7 +59,7 @@ void HE60rCover::endstop_reached_(CoverOperation operation) {
if (this->last_command_ == operation) {
float dur = (float) (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur);
operation == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
}
this->publish_state();
}
@@ -213,9 +213,9 @@ void HE60rCover::start_direction_(CoverOperation dir) {
if (this->current_operation == dir)
return;
ESP_LOGD(TAG, "'%s' - Direction '%s' requested.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? "OPEN"
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
: "STOP");
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
: dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
: LOG_STR_LITERAL("STOP"));
if (dir == this->next_direction_) {
// either moving and needs to stop, or stopped and will move correctly on one trigger
+2 -1
View File
@@ -336,7 +336,8 @@ void HlkFm22xComponent::dump_config() {
}
if (this->enrolling_binary_sensor_) {
LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF");
ESP_LOGCONFIG(TAG, " Current Value: %s",
this->enrolling_binary_sensor_->state ? LOG_STR_LITERAL("ON") : LOG_STR_LITERAL("OFF"));
}
if (this->face_count_sensor_) {
LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
+2 -1
View File
@@ -97,7 +97,8 @@ void HLW8012Component::update() {
if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) {
this->current_mode_ = !this->current_mode_;
ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE");
ESP_LOGV(TAG, "Changing mode to %s mode",
this->current_mode_ ? LOG_STR_LITERAL("CURRENT") : LOG_STR_LITERAL("VOLTAGE"));
this->change_mode_at_ = 0;
this->sel_pin_->digital_write(this->current_mode_);
}
@@ -116,8 +116,8 @@ void ILI9XXXDisplay::dump_config() {
" Mirror_x: %s\n"
" Mirror_y: %s\n"
" Invert colors: %s",
this->color_order_ == display::COLOR_ORDER_BGR ? "BGR" : "RGB", YESNO(this->swap_xy_),
YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->pre_invertcolors_));
this->color_order_ == display::COLOR_ORDER_BGR ? LOG_STR_LITERAL("BGR") : LOG_STR_LITERAL("RGB"),
YESNO(this->swap_xy_), YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->pre_invertcolors_));
if (this->is_failed()) {
ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!");
+1 -1
View File
@@ -16,7 +16,7 @@ from esphome.types import ConfigType
AUTO_LOAD = ["improv_base"]
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["logger", "wifi"]
DEPENDENCIES = ["logger", "network"]
improv_serial_ns = cg.esphome_ns.namespace("improv_serial")
@@ -1,5 +1,5 @@
#include "improv_serial_component.h"
#ifdef USE_WIFI
#ifdef USE_IMPROV_SERIAL
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
@@ -7,7 +7,10 @@
#include "esphome/core/version.h"
#include "esphome/components/logger/logger.h"
#include "esphome/components/network/util.h"
#ifdef USE_WIFI
#include "esphome/components/wifi/scan_list.h"
#endif
#include <array>
@@ -26,13 +29,17 @@ void ImprovSerialComponent::setup() {
this->hw_serial_ = logger::global_logger->get_hw_serial();
#endif
if (wifi::global_wifi_component->has_sta()) {
// The Improv state machine tracks Wi-Fi provisioning only. General device
// connectivity (e.g. Ethernet) is reported separately via GET_NETWORK_STATE.
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->has_sta()) {
this->state_ = improv::STATE_PROVISIONED;
} else if (!wifi::global_wifi_component->is_disabled()) {
} else if (wifi::global_wifi_component != nullptr && !wifi::global_wifi_component->is_disabled()) {
// Respect Wi-Fi's disabled state; forcing a scan while disabled throws
// the wifi component into an invalid state from which it cannot recover.
wifi::global_wifi_component->start_scanning();
}
#endif
}
void ImprovSerialComponent::loop() {
@@ -55,8 +62,14 @@ void ImprovSerialComponent::loop() {
}
}
if (this->state_ == improv::STATE_PROVISIONING) {
if (wifi::global_wifi_component->is_connected()) {
#ifdef USE_WIFI
if (this->state_ == improv::STATE_PROVISIONING && wifi::global_wifi_component != nullptr &&
wifi::global_wifi_component->is_connected()) {
// Being connected is not enough: re-provisioning a device that is already online leaves the
// prior network up until it drops, so check that the joined network is the requested one
// before reporting success. Same test as the wifi.connect action.
char ssid_buf[wifi::SSID_BUFFER_SIZE];
if (strcmp(wifi::global_wifi_component->wifi_ssid_to(ssid_buf), this->connecting_sta_.get_ssid().c_str()) == 0) {
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
this->connecting_sta_.get_password());
this->connecting_sta_ = {};
@@ -66,6 +79,7 @@ void ImprovSerialComponent::loop() {
this->send_settings_response_(improv::WIFI_SETTINGS);
}
}
#endif
}
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
@@ -143,15 +157,17 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size)
#endif
}
void ImprovSerialComponent::send_settings_response_(improv::Command command) {
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, command);
#ifdef USE_IMPROV_SERIAL_NEXT_URL
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
#endif
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
void ImprovSerialComponent::add_webserver_urls_(improv::RpcResponseBuilder &builder, [[maybe_unused]] bool wifi_first) {
// The webserver listens on every interface, so advertise each one that has a usable IPv4.
// network::get_ip_addresses() can't be used here: it returns only the highest-priority
// interface's addresses, which are all-unset (0.0.0.0) when e.g. Ethernet has no link while
// the device is online via Wi-Fi, and 0.0.0.0 must not become the advertised URL. OpenThread
// is omitted: it only ever has IPv6 addresses, which cannot form an IPv4 http:// URL.
const auto append_urls = [&builder](const network::IPAddresses &addresses) {
for (const auto &ip : addresses) {
if (!ip.is_ip4() || !ip.is_set())
continue;
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
ip.str_to(ip_buf);
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
@@ -162,9 +178,43 @@ void ImprovSerialComponent::send_settings_response_(improv::Command command) {
if (!builder.add_string(webserver_url, len)) {
ESP_LOGW(TAG, "Response full; URL dropped");
}
break;
}
}
};
#ifdef USE_WIFI
// Clients redirect to the first URL, so the interface the client just configured has to lead:
// another interface's address can be on a subnet that client cannot reach.
const auto append_wifi_urls = [&append_urls]() {
if (wifi::global_wifi_component != nullptr)
append_urls(wifi::global_wifi_component->get_ip_addresses());
};
if (wifi_first)
append_wifi_urls();
#endif
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr)
append_urls(ethernet::global_eth_component->get_ip_addresses());
#endif
#ifdef USE_MODEM
if (modem::global_modem_component != nullptr)
append_urls(modem::global_modem_component->get_ip_addresses());
#endif
#ifdef USE_WIFI
if (!wifi_first)
append_wifi_urls();
#endif
}
#endif // USE_WEBSERVER
void ImprovSerialComponent::send_settings_response_(improv::Command command) {
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, command);
#ifdef USE_IMPROV_SERIAL_NEXT_URL
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
#endif
#ifdef USE_WEBSERVER
// This response only ever answers Wi-Fi provisioning, so lead with the Wi-Fi URL as it did
// before other interfaces were reported.
this->add_webserver_urls_(builder, /*wifi_first=*/true);
#endif
this->send_response_(builder.finish(false));
}
@@ -231,7 +281,8 @@ bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) {
switch (command.command) {
case improv::WIFI_SETTINGS: {
if (wifi::global_wifi_component->is_disabled()) {
#ifdef USE_WIFI
if (wifi::global_wifi_component == nullptr || wifi::global_wifi_component->is_disabled()) {
// Wi-Fi is disabled, so we can't provision. Respond immediately
// instead of letting the client wait out its provisioning timeout.
ESP_LOGW(TAG, "Wi-Fi is disabled; cannot provision");
@@ -243,21 +294,32 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
sta.set_password(command.password.c_str());
this->connecting_sta_ = sta;
// Sampled before start_connecting(): the old connection drops asynchronously after it.
const bool switching = wifi::global_wifi_component->is_connected();
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_connecting(sta);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());
this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
this->set_timeout("wifi-connect-timeout", switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS,
[this]() { this->on_wifi_connect_timeout_(); });
#else
// No Wi-Fi support compiled in; there is nothing to provision.
ESP_LOGW(TAG, "Wi-Fi not supported; cannot provision");
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
#endif
return true;
}
case improv::GET_CURRENT_STATE:
if (wifi::global_wifi_component->is_disabled()) {
// Wi-Fi is disabled; report the Improv "stopped" state so a client can tell
// the user that provisioning is unavailable. Reported transiently without
// disturbing our internal provisioning state machine, so a later `wifi.enable`
// still reports the correct state.
case improv::GET_CURRENT_STATE: {
// This state machine tracks Wi-Fi provisioning only. When Wi-Fi is disabled or not
// compiled in, provisioning is unavailable -> report STOPPED so the client doesn't
// offer a Wi-Fi form. General connectivity (e.g. Ethernet) is reported separately
// via GET_NETWORK_STATE.
#ifdef USE_WIFI
if (wifi::global_wifi_component == nullptr || wifi::global_wifi_component->is_disabled()) {
// Reported transiently without disturbing our internal provisioning state machine,
// so a later `wifi.enable` still reports the correct state.
this->send_current_state_(improv::STATE_STOPPED);
return true;
}
@@ -265,14 +327,20 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
if (this->state_ == improv::STATE_PROVISIONED) {
this->send_settings_response_(improv::GET_CURRENT_STATE);
}
#else
this->send_current_state_(improv::STATE_STOPPED);
#endif
return true;
}
case improv::GET_DEVICE_INFO: {
this->send_version_info_();
return true;
}
case improv::GET_WIFI_NETWORKS: {
const auto &results = wifi::global_wifi_component->get_scan_result();
// Declared out here because the terminating empty response is sent with or without Wi-Fi
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
#ifdef USE_WIFI
const auto &results = wifi::global_wifi_component->get_scan_result();
for (const auto &scan : results) {
bool with_auth = false;
if (!wifi::should_show_scan_entry(results, scan, with_auth))
@@ -289,11 +357,52 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
builder.add_string(YESNO(with_auth));
this->send_response_(builder.finish(false));
}
#endif // USE_WIFI
// Send empty response to signify the end of the list.
improv::RpcResponseBuilder builder(buf, improv::GET_WIFI_NETWORKS);
this->send_response_(builder.finish(false));
return true;
}
case improv::GET_NETWORK_STATE: {
// Reports general device connectivity and which network interfaces are present, decoupled
// from the Wi-Fi-only provisioning state machine. data[0] is a decimal flags byte;
// when online, the reachable device URL(s) follow.
uint8_t flags = 0;
if (network::is_connected())
flags |= improv::NETWORK_IS_ONLINE;
#ifdef USE_WIFI
flags |= improv::NETWORK_SUPPORTS_WIFI;
#endif
#ifdef USE_ETHERNET
flags |= improv::NETWORK_SUPPORTS_ETHERNET;
#endif
#ifdef USE_OPENTHREAD
flags |= improv::NETWORK_SUPPORTS_THREAD;
#endif
#ifdef USE_MODEM
flags |= improv::NETWORK_SUPPORTS_MODEM;
#endif
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, improv::GET_NETWORK_STATE);
// Every flag bit fits int8_t's positive range, so int8_to_str renders the byte
static_assert(improv::NETWORK_SUPPORTS_MODEM <= 0x7F, "network flags no longer fit int8_to_str");
char flags_buf[4]; // uint8_t: max "255" + null
char *flags_end = int8_to_str(flags_buf, static_cast<int8_t>(flags));
builder.add_string(flags_buf, flags_end - flags_buf);
#ifdef USE_WEBSERVER
// Not tied to one interface, so follow the configured priority the way
// network::get_ip_addresses() does: a wifi-first network priority list leads with Wi-Fi.
if (flags & improv::NETWORK_IS_ONLINE) {
#if defined(USE_NETWORK_PRIMARY_INTERFACE_WIFI) && defined(USE_WIFI)
this->add_webserver_urls_(builder, /*wifi_first=*/true);
#else
this->add_webserver_urls_(builder, /*wifi_first=*/false);
#endif
}
#endif
this->send_response_(builder.finish(false));
return true;
}
default: {
ESP_LOGW(TAG, "Unknown payload");
this->set_error_(improv::ERROR_UNKNOWN_RPC);
@@ -331,12 +440,14 @@ void ImprovSerialComponent::send_response_(std::span<const uint8_t> response) {
this->write_data_(response.data(), response.size());
}
#ifdef USE_WIFI
void ImprovSerialComponent::on_wifi_connect_timeout_() {
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
this->set_state_(improv::STATE_AUTHORIZED);
ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
wifi::global_wifi_component->clear_sta();
}
#endif
ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -2,15 +2,19 @@
#include "esphome/components/improv_base/improv_base.h"
#include "esphome/components/logger/logger.h"
#include "esphome/components/wifi/wifi_component.h"
#include "esphome/components/network/util.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_WIFI
#ifdef USE_IMPROV_SERIAL
#include <improv.h>
#include <span>
#include <vector>
#ifdef USE_WIFI
#include "esphome/components/wifi/wifi_component.h"
#endif
#ifdef USE_IMPROV_SERIAL_UART
#include "esphome/components/uart/uart_component.h"
#elif defined(USE_ESP32)
@@ -48,13 +52,22 @@ enum ImprovSerialType : uint8_t {
static const uint16_t IMPROV_SERIAL_TIMEOUT = 100;
static const uint8_t IMPROV_SERIAL_VERSION = 1;
#ifdef USE_WIFI
// Wi-Fi connect failure timers: a fresh provision reports at 30 s (stock behavior), while
// switching networks on an already-connected device (disconnect + reconnect) can legitimately
// take longer; 90 s matches esp32_improv's default wifi_timeout.
static const uint32_t WIFI_CONNECT_TIMEOUT_MS = 30000;
static const uint32_t WIFI_SWITCH_TIMEOUT_MS = 90000;
#endif
// The serial frame length field is one byte
static constexpr size_t MAX_SERIAL_RESPONSE = 255;
// command + data length + trailing byte
static constexpr size_t RPC_RESPONSE_OVERHEAD = 3;
static constexpr size_t MAX_SERIAL_PAYLOAD = MAX_SERIAL_RESPONSE - RPC_RESPONSE_OVERHEAD;
#ifdef USE_WEBSERVER
// length byte + "http://" + IPv4 + ":" + port
// length byte + "http://" + IPv4 + ":" + port. Reserves the first URL only; a device with
// several interfaces online adds the rest best-effort and warns if one no longer fits.
static constexpr size_t WEBSERVER_URL_RESERVE = 1 + 7 + 15 + 1 + 5;
#else
static constexpr size_t WEBSERVER_URL_RESERVE = 0;
@@ -84,8 +97,15 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
void send_current_state_(improv::State state);
void set_error_(improv::Error error);
void send_response_(std::span<const uint8_t> response);
#ifdef USE_WIFI
void on_wifi_connect_timeout_();
#endif
#ifdef USE_WEBSERVER
/// Append one web server URL per interface that has a usable IPv4. With wifi_first the Wi-Fi
/// URL leads, for responses to Wi-Fi provisioning; otherwise interfaces go in priority order.
void add_webserver_urls_(improv::RpcResponseBuilder &builder, [[maybe_unused]] bool wifi_first);
#endif
void send_settings_response_(improv::Command command);
void send_version_info_();
@@ -167,7 +187,9 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
std::vector<uint8_t> rx_buffer_;
uint32_t last_read_byte_{0};
#ifdef USE_WIFI
wifi::WiFiAP connecting_sta_;
#endif
improv::State state_{improv::STATE_AUTHORIZED};
};
@@ -209,7 +209,8 @@ void INA2XX::dump_config() {
" CURRENT_LSB = %f\n"
" SHUNT_CAL = %d",
this->shunt_resistance_ohm_, this->max_current_a_, this->shunt_tempco_ppm_c_,
(uint8_t) this->adc_range_, this->adc_range_ ? "±40.96 mV" : "±163.84 mV", this->current_lsb_,
(uint8_t) this->adc_range_,
this->adc_range_ ? LOG_STR_LITERAL("±40.96 mV") : LOG_STR_LITERAL("±163.84 mV"), this->current_lsb_,
this->shunt_cal_);
ESP_LOGCONFIG(TAG, " ADC Samples = %d; ADC times: Bus = %d μs, Shunt = %d μs, Temp = %d μs",
@@ -1,5 +1,7 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
from esphome.components.esp32.const import VARIANT_ESP32
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
@@ -48,6 +50,10 @@ async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32:
# temprature_sens_read() lives in the esp_phy blob, which is excluded by default
include_builtin_idf_component("esp_phy")
if CORE.using_zephyr and CORE.is_nrf52:
zephyr_add_prj_conf("SENSOR", True)
zephyr_add_prj_conf("TEMP_NRF5", True)
+22 -20
View File
@@ -740,7 +740,7 @@ bool IT8951Display::prepare_update_region_(UpdateMode &mode) {
this->reset_dirty_region_();
ESP_LOGV(TAG, "Update: %ux%u@%u,%u mode=%u (%s)", width, height, x, y, static_cast<unsigned>(mode),
this->grayscale_ ? "grayscale" : "mono");
this->grayscale_ ? LOG_STR_LITERAL("grayscale") : LOG_STR_LITERAL("mono"));
return true;
}
@@ -1063,25 +1063,27 @@ void IT8951Display::dump_config() {
strncpy(force_temperature, "(controller default)", sizeof(force_temperature));
force_temperature[sizeof(force_temperature) - 1] = '\0';
}
ESP_LOGCONFIG(TAG,
" Model preset: %s"
"\n Dimensions: %dx%d"
"\n Buffer: %u bytes"
"\n Image buffer addr: 0x%04X%04X"
"\n VCOM: %.02fV (set selector 0x%04X)"
"\n Force temperature: %s"
"\n Display command: %s"
"\n Sleep when done: %s"
"\n Full update every: %u"
"\n Inverted colors: %s"
"\n Pixel format: %s"
"\n Reset duration: %" PRIu32 "ms",
this->name_ != nullptr ? this->name_ : "(unknown)", this->get_width_internal(),
this->get_height_internal(), static_cast<unsigned>(this->buffer_length_), this->img_buf_addr_h_,
this->img_buf_addr_l_, static_cast<float>(this->vcom_) / 1000.0f, this->vcom_register_,
force_temperature, this->use_legacy_dpy_area_ ? "DPY_AREA (0x0034, legacy)" : "DPY_BUF_AREA (0x0037)",
YESNO(this->sleep_when_done_), this->full_update_every_, YESNO(this->invert_colors_),
this->grayscale_ ? "4bpp grayscale" : "1bpp monochrome", this->reset_duration_);
ESP_LOGCONFIG(
TAG,
" Model preset: %s"
"\n Dimensions: %dx%d"
"\n Buffer: %u bytes"
"\n Image buffer addr: 0x%04X%04X"
"\n VCOM: %.02fV (set selector 0x%04X)"
"\n Force temperature: %s"
"\n Display command: %s"
"\n Sleep when done: %s"
"\n Full update every: %u"
"\n Inverted colors: %s"
"\n Pixel format: %s"
"\n Reset duration: %" PRIu32 "ms",
this->name_ != nullptr ? this->name_ : LOG_STR_LITERAL("(unknown)"), this->get_width_internal(),
this->get_height_internal(), static_cast<unsigned>(this->buffer_length_), this->img_buf_addr_h_,
this->img_buf_addr_l_, static_cast<float>(this->vcom_) / 1000.0f, this->vcom_register_, force_temperature,
this->use_legacy_dpy_area_ ? LOG_STR_LITERAL("DPY_AREA (0x0034, legacy)")
: LOG_STR_LITERAL("DPY_BUF_AREA (0x0037)"),
YESNO(this->sleep_when_done_), this->full_update_every_, YESNO(this->invert_colors_),
this->grayscale_ ? LOG_STR_LITERAL("4bpp grayscale") : LOG_STR_LITERAL("1bpp monochrome"), this->reset_duration_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_PIN(" CS Pin: ", this->cs_);
+28 -41
View File
@@ -1,87 +1,74 @@
#include "kuntze.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::kuntze {
static const char *const TAG = "kuntze";
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
static constexpr uint16_t REGISTER_PH = 4136;
static constexpr uint16_t REGISTER_TEMPERATURE = 4160;
static constexpr uint16_t REGISTER_DIS1 = 4680;
static constexpr uint16_t REGISTER_DIS2 = 6000;
static constexpr uint16_t REGISTER_REDOX = 4688;
static constexpr uint16_t REGISTER_EC = 4728;
static constexpr uint16_t REGISTER_OCI = 5832;
static constexpr uint16_t REGISTER[] = {REGISTER_PH, REGISTER_TEMPERATURE, REGISTER_DIS1, REGISTER_DIS2,
REGISTER_REDOX, REGISTER_EC, REGISTER_OCI};
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
void Kuntze::on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) {
if (!modbus::succeeded(status) || registers.size() < 2)
return;
void Kuntze::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
auto data = modbus::helpers::server_pdu_payload(response_pdu);
auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); };
// Each value is a register pair: the reading, then the number of decimal places in its low byte.
float value = registers[0];
for (uint16_t i = 0; i < (registers[1] & 0xFF); i++)
value /= 10.0f;
this->waiting_ = false;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(KUNTZE_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Data: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
float value = (float) get_16bit(0);
for (int i = 0; i < data[3]; i++)
value /= 10.0;
switch (this->state_) {
case 1:
switch (start_address) {
case REGISTER_PH:
ESP_LOGD(TAG, "pH=%.1f", value);
if (this->ph_sensor_ != nullptr)
this->ph_sensor_->publish_state(value);
break;
case 2:
case REGISTER_TEMPERATURE:
ESP_LOGD(TAG, "temperature=%.1f", value);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(value);
break;
case 3:
case REGISTER_DIS1:
ESP_LOGD(TAG, "DIS1=%.1f", value);
if (this->dis1_sensor_ != nullptr)
this->dis1_sensor_->publish_state(value);
break;
case 4:
case REGISTER_DIS2:
ESP_LOGD(TAG, "DIS2=%.1f", value);
if (this->dis2_sensor_ != nullptr)
this->dis2_sensor_->publish_state(value);
break;
case 5:
case REGISTER_REDOX:
ESP_LOGD(TAG, "REDOX=%.1f", value);
if (this->redox_sensor_ != nullptr)
this->redox_sensor_->publish_state(value);
break;
case 6:
case REGISTER_EC:
ESP_LOGD(TAG, "EC=%.1f", value);
if (this->ec_sensor_ != nullptr)
this->ec_sensor_->publish_state(value);
break;
case 7:
case REGISTER_OCI:
ESP_LOGD(TAG, "OCI=%.1f", value);
if (this->oci_sensor_ != nullptr)
this->oci_sensor_->publish_state(value);
break;
}
if (++this->state_ > 7)
this->state_ = 0;
}
void Kuntze::loop() {
uint32_t now = App.get_loop_component_start_time();
// timeout after 15 seconds
if (this->waiting_ && (now - this->last_send_ > 15000)) {
ESP_LOGW(TAG, "timed out waiting for response");
this->waiting_ = false;
}
if (this->waiting_ || (this->state_ == 0))
return;
this->last_send_ = now;
this->read_holding_registers(REGISTER[this->state_ - 1], 2);
this->waiting_ = true;
void Kuntze::update() {
for (uint16_t reg : REGISTER)
this->read_holding_registers(reg, 2);
}
void Kuntze::update() { this->state_ = 1; }
void Kuntze::dump_config() {
ESP_LOGCONFIG(TAG,
"Kuntze:\n"
+2 -6
View File
@@ -18,18 +18,14 @@ class Kuntze final : public PollingComponent, public modbus::ModbusClientDevice
void set_ec_sensor(sensor::Sensor *ec_sensor) { ec_sensor_ = ec_sensor; }
void set_oci_sensor(sensor::Sensor *oci_sensor) { oci_sensor_ = oci_sensor; }
void loop() override;
void update() override;
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) override;
void dump_config() override;
protected:
int state_{0};
bool waiting_{false};
uint32_t last_send_{0};
sensor::Sensor *ph_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *dis1_sensor_{nullptr};
+2 -1
View File
@@ -150,7 +150,8 @@ void Lc709203f::dump_config() {
" Pack Size: %d mAH\n"
" Pack APA: 0x%02X\n"
" Pack Rated Voltage: 3.%sV",
this->pack_size_, this->apa_, this->pack_voltage_ == 0x0000 ? "8" : "7");
this->pack_size_, this->apa_,
this->pack_voltage_ == 0x0000 ? LOG_STR_LITERAL("8") : LOG_STR_LITERAL("7"));
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
+2 -1
View File
@@ -701,7 +701,8 @@ uint8_t LD2420Component::set_config_mode(bool enable) {
cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER);
}
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command);
ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"),
cmd_frame.command);
return this->send_cmd_from_array(cmd_frame);
}
+2 -1
View File
@@ -437,7 +437,8 @@ void LD6002BComponent::dump_config() {
"HLK-LD6002B:\n"
" Auto wake: %s\n"
" Max data length: %u",
this->auto_wake_ ? "true" : "false", static_cast<unsigned>(this->max_data_len_));
this->auto_wake_ ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
static_cast<unsigned>(this->max_data_len_));
if (this->wakeup_pin_ != nullptr) {
LOG_PIN(" Wake-up Pin: ", this->wakeup_pin_);
ESP_LOGCONFIG(TAG, " Wake Pulse: %" PRIu32 "ms", this->wakeup_pulse_ms_);
@@ -5,7 +5,11 @@ namespace esphome::light {
uint8_t ESPColorCorrection::gamma_correct_(uint8_t value) const {
if (this->gamma_table_ == nullptr)
return value;
return static_cast<uint8_t>((progmem_read_uint16(&this->gamma_table_[value]) + 128) / 257);
uint16_t table_value = progmem_read_uint16(&this->gamma_table_[value]);
uint8_t result = (table_value + 128) / 257;
if (result == 0 && table_value != 0)
return 1;
return result;
}
uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const {
+2 -1
View File
@@ -483,6 +483,7 @@ LV_ANIM = LvConstant(
LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER")
LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF")
LV_GRAD_EXTEND = LvConstant("LV_GRAD_EXTEND_", "PAD", "REPEAT", "REFLECT")
LV_LOG_LEVELS = {
"VERBOSE": "TRACE",
@@ -904,7 +905,7 @@ LV_COLOR_FORMATS = (
LV_DEFINES = (
"LV_USE_FREERTOS_TASK_NOTIFY", "LV_DRAW_BUF_STRIDE_ALIGN", "LV_USE_DRAW_SW", "LV_DRAW_SW_DRAW_UNIT_CNT",
"LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D",
"LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_SW_COMPLEX_GRADIENTS", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D",
"LV_USE_G2D_DRAW_THREAD", "LV_VG_LITE_USE_BOX_SHADOW", "LV_VG_LITE_THORVG_16PIXELS_ALIGN", "LV_LOG_USE_TIMESTAMP",
"LV_LOG_USE_FILE_LINE", "LV_USE_OBJ_ID_BUILTIN", "LV_USE_OBJ_PROPERTY_NAME", "LV_ATTRIBUTE_MEM_ALIGN_SIZE",
"LV_FONT_MONTSERRAT_14", "LV_USE_FONT_PLACEHOLDER", "LV_WIDGETS_HAS_DEFAULT_VALUE", "LV_USE_ARCLABEL",
+172 -23
View File
@@ -13,18 +13,40 @@ from esphome.core import ID
from esphome.cpp_generator import MockObj
from .defines import (
CONF_END_ANGLE,
CONF_GRADIENTS,
CONF_OPA,
CONF_START_ANGLE,
LV_DITHER,
LV_GRAD_EXTEND,
add_define,
add_lv_use,
add_warning,
)
from .lv_validation import lv_color, lv_percentage, opacity
from .lv_validation import (
lv_angle_degrees,
lv_color,
lv_percentage,
opacity,
pixels_or_percent,
)
from .lvcode import lv
from .types import lv_color_t, lv_gradient_t, lv_opa_t
CONF_STOPS = "stops"
CONF_LINEAR = "linear"
CONF_RADIAL = "radial"
CONF_CONICAL = "conical"
CONF_EXTEND = "extend"
CONF_FROM_X = "from_x"
CONF_FROM_Y = "from_y"
CONF_TO_X = "to_x"
CONF_TO_Y = "to_y"
CONF_CENTER_X = "center_x"
CONF_CENTER_Y = "center_y"
CONF_FOCAL_X = "focal_x"
CONF_FOCAL_Y = "focal_y"
CONF_FOCAL_RADIUS = "focal_radius"
def min_stops(value):
@@ -33,27 +55,109 @@ def min_stops(value):
return value
STOPS_SCHEMA = cv.All(
[
cv.Schema(
{
cv.Required(CONF_COLOR): lv_color,
cv.Optional(CONF_OPA, default=1.0): opacity,
cv.Required(CONF_POSITION): lv_percentage,
}
)
],
min_stops,
)
LINEAR_SCHEMA = cv.Schema(
{
cv.Required(CONF_FROM_X): pixels_or_percent,
cv.Required(CONF_FROM_Y): pixels_or_percent,
cv.Required(CONF_TO_X): pixels_or_percent,
cv.Required(CONF_TO_Y): pixels_or_percent,
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
}
)
RADIAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_CENTER_X): pixels_or_percent,
cv.Required(CONF_CENTER_Y): pixels_or_percent,
cv.Required(CONF_TO_X): pixels_or_percent,
cv.Required(CONF_TO_Y): pixels_or_percent,
cv.Optional(CONF_FOCAL_X): pixels_or_percent,
cv.Optional(CONF_FOCAL_Y): pixels_or_percent,
# No default: gradient_validator() must be able to tell whether this was actually
# given, to require it alongside focal_x/focal_y rather than silently drop it.
# LVGL's lv_grad_radial_set_focal() takes this as a scalar, not lv_pct() -
# unlike every other coordinate here, a percentage is not accepted.
cv.Optional(CONF_FOCAL_RADIUS): cv.positive_int,
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
}
)
CONICAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_CENTER_X): pixels_or_percent,
cv.Required(CONF_CENTER_Y): pixels_or_percent,
cv.Optional(CONF_START_ANGLE, default=0): lv_angle_degrees,
cv.Optional(CONF_END_ANGLE, default=360): lv_angle_degrees,
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
}
)
def gradient_validator(config):
direction = config[CONF_DIRECTION]
for gradient_direction, key in (
("LINEAR", CONF_LINEAR),
("RADIAL", CONF_RADIAL),
("CONICAL", CONF_CONICAL),
):
if direction == gradient_direction:
if key not in config:
raise cv.Invalid(
f"'{key}' is required for {gradient_direction} gradient direction"
)
elif key in config:
raise cv.Invalid(
f"'{key}' is only valid with 'direction: {gradient_direction}'"
)
if CONF_RADIAL in config:
radial = config[CONF_RADIAL]
has_focal_x = CONF_FOCAL_X in radial
has_focal_y = CONF_FOCAL_Y in radial
has_focal_radius = CONF_FOCAL_RADIUS in radial
if has_focal_x != has_focal_y or (has_focal_radius and not has_focal_x):
raise cv.Invalid(
"'focal_x', 'focal_y' and 'focal_radius' must be specified together "
"in 'radial'"
)
return config
GRADIENT_SCHEMA = cv.ensure_list(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
cv.Required(CONF_DIRECTION): cv.one_of(
"HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True
),
cv.Optional(CONF_DITHER): LV_DITHER.one_of,
cv.Required(CONF_STOPS): cv.All(
[
cv.Schema(
{
cv.Required(CONF_COLOR): lv_color,
cv.Optional(CONF_OPA, default=1.0): opacity,
cv.Required(CONF_POSITION): lv_percentage,
}
)
],
min_stops,
),
}
cv.All(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
cv.Required(CONF_DIRECTION): cv.one_of(
"HOR",
"HORIZONTAL",
"VER",
"VERTICAL",
"LINEAR",
"RADIAL",
"CONICAL",
upper=True,
),
cv.Optional(CONF_DITHER): LV_DITHER.one_of,
cv.Optional(CONF_LINEAR): LINEAR_SCHEMA,
cv.Optional(CONF_RADIAL): RADIAL_SCHEMA,
cv.Optional(CONF_CONICAL): CONICAL_SCHEMA,
cv.Required(CONF_STOPS): STOPS_SCHEMA,
}
),
gradient_validator,
)
)
@@ -65,15 +169,60 @@ async def gradients_to_code(config):
add_warning(
"The 'dither' option for gradients is not supported by LVGL 9.x and will be ignored"
)
if any(
x[CONF_DIRECTION] in ("LINEAR", "RADIAL", "CONICAL")
for x in config.get(CONF_GRADIENTS, ())
):
# LVGL's software renderer only draws these gradient types when this is enabled; without
# it they silently fall back to a plain horizontal gradient.
add_define("LV_USE_DRAW_SW_COMPLEX_GRADIENTS")
for gradient in config.get(CONF_GRADIENTS, ()):
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
idbase = gradient[CONF_ID].id
stops = sorted(gradient[CONF_STOPS], key=itemgetter(CONF_POSITION))
max_stops = max(max_stops, len(stops))
if gradient[CONF_DIRECTION].startswith("VER"):
direction = gradient[CONF_DIRECTION]
if direction.startswith("VER"):
lv.grad_vertical_init(var)
else:
elif direction.startswith("HOR"):
lv.grad_horizontal_init(var)
elif direction == "LINEAR":
linear = gradient[CONF_LINEAR]
lv.grad_linear_init(
var,
await pixels_or_percent.process(linear[CONF_FROM_X]),
await pixels_or_percent.process(linear[CONF_FROM_Y]),
await pixels_or_percent.process(linear[CONF_TO_X]),
await pixels_or_percent.process(linear[CONF_TO_Y]),
await LV_GRAD_EXTEND.process(linear[CONF_EXTEND]),
)
elif direction == "RADIAL":
radial = gradient[CONF_RADIAL]
lv.grad_radial_init(
var,
await pixels_or_percent.process(radial[CONF_CENTER_X]),
await pixels_or_percent.process(radial[CONF_CENTER_Y]),
await pixels_or_percent.process(radial[CONF_TO_X]),
await pixels_or_percent.process(radial[CONF_TO_Y]),
await LV_GRAD_EXTEND.process(radial[CONF_EXTEND]),
)
if CONF_FOCAL_X in radial:
lv.grad_radial_set_focal(
var,
await pixels_or_percent.process(radial[CONF_FOCAL_X]),
await pixels_or_percent.process(radial[CONF_FOCAL_Y]),
radial.get(CONF_FOCAL_RADIUS, 0),
)
elif direction == "CONICAL":
conical = gradient[CONF_CONICAL]
lv.grad_conical_init(
var,
await pixels_or_percent.process(conical[CONF_CENTER_X]),
await pixels_or_percent.process(conical[CONF_CENTER_Y]),
await lv_angle_degrees.process(conical[CONF_START_ANGLE]),
await lv_angle_degrees.process(conical[CONF_END_ANGLE]),
await LV_GRAD_EXTEND.process(conical[CONF_EXTEND]),
)
stop_colors = cg.static_const_array(
ID(idbase + "_colors_", type=lv_color_t),
[await lv_color.process(x[CONF_COLOR]) for x in stops],
+4 -2
View File
@@ -23,8 +23,10 @@ void MAX31856Sensor::setup() {
void MAX31856Sensor::dump_config() {
LOG_SENSOR("", "MAX31856", this);
LOG_PIN(" CS Pin: ", this->cs_);
ESP_LOGCONFIG(TAG, " Mains Filter: %s",
(filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!")));
ESP_LOGCONFIG(
TAG, " Mains Filter: %s",
(filter_ == FILTER_60HZ ? LOG_STR_LITERAL("60 Hz")
: (filter_ == FILTER_50HZ ? LOG_STR_LITERAL("50 Hz") : LOG_STR_LITERAL("Unknown!"))));
if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) {
ESP_LOGCONFIG(TAG, " Thermocouple Type: Unknown");
} else {
+8 -6
View File
@@ -80,12 +80,14 @@ void MAX31865Sensor::dump_config() {
LOG_SENSOR("", "MAX31865", this);
LOG_PIN(" CS Pin: ", this->cs_);
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG,
" Reference Resistance: %.2fΩ\n"
" RTD: %u-wire %.2fΩ\n"
" Mains Filter: %s",
reference_resistance_, rtd_wires_, rtd_nominal_resistance_,
(filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!")));
ESP_LOGCONFIG(
TAG,
" Reference Resistance: %.2fΩ\n"
" RTD: %u-wire %.2fΩ\n"
" Mains Filter: %s",
reference_resistance_, rtd_wires_, rtd_nominal_resistance_,
(filter_ == FILTER_60HZ ? LOG_STR_LITERAL("60 Hz")
: (filter_ == FILTER_50HZ ? LOG_STR_LITERAL("50 Hz") : LOG_STR_LITERAL("Unknown!"))));
}
void MAX31865Sensor::read_data_() {
+3 -2
View File
@@ -319,7 +319,7 @@ uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx, bool *ok) {
if (!(this->read_16_(reg, &buf))) {
this->error_code_ = MCP4461_STATUS_I2C_ERROR;
this->status_set_warning();
ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? "nonvolatile " : "", wiper_idx);
ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? LOG_STR_LITERAL("nonvolatile ") : "", wiper_idx);
return 0;
}
if (ok != nullptr) {
@@ -377,7 +377,8 @@ void Mcp4461Component::write_wiper_level_(uint8_t wiper, uint16_t value) {
if (!(this->mcp4461_write_(this->get_wiper_address_(wiper), value, nonvolatile))) {
this->error_code_ = MCP4461_STATUS_I2C_ERROR;
this->status_set_warning();
ESP_LOGW(TAG, "Error writing %swiper %u level %u", (wiper > 3) ? "nonvolatile " : "", wiper, value);
ESP_LOGW(TAG, "Error writing %swiper %u level %u", (wiper > 3) ? LOG_STR_LITERAL("nonvolatile ") : "", wiper,
value);
}
}
@@ -122,7 +122,7 @@ void MediaPlayerCall::perform() {
ESP_LOGV(TAG, " Volume: %.2f", this->volume_.value());
}
if (this->announcement_.has_value()) {
ESP_LOGV(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no");
ESP_LOGV(TAG, " Announcement: %s", this->announcement_.value() ? LOG_STR_LITERAL("yes") : LOG_STR_LITERAL("no"));
}
this->parent_->control(*this);
}
+7 -2
View File
@@ -12,7 +12,12 @@ from esphome.components.const import (
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S3, only_on_variant
from esphome.components.esp32 import (
VARIANT_ESP32P4,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
only_on_variant,
)
from esphome.components.mipi import (
COLOR_ORDERS,
CONF_DE_PIN,
@@ -226,7 +231,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
config = cv.All(
schema,
cv.only_on_esp32,
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4, VARIANT_ESP32S31]),
)(config)
model = MODELS[config[CONF_MODEL].upper()]
model.check_requirements()
+3 -2
View File
@@ -1,4 +1,4 @@
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
#include "mipi_rgb.h"
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
@@ -400,4 +400,5 @@ void MipiRgb::dump_config() {
}
} // namespace esphome::mipi_rgb
#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) ||
// defined(USE_ESP32_VARIANT_ESP32S31)
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
#include "esphome/core/gpio.h"
#include "esphome/components/display/display.h"
#include "esp_lcd_panel_ops.h"
@@ -0,0 +1,28 @@
from . import RgbDriverChip
# fmt: off
RgbDriverChip(
"CROWPANEL-ADVANCE-7",
requires={"psram"},
initsequence=(),
pclk_frequency="20MHz",
hsync_pulse_width=4,
hsync_front_porch=8,
hsync_back_porch=8,
vsync_pulse_width=4,
vsync_front_porch=8,
vsync_back_porch=8,
pclk_inverted=True,
color_order="RGB",
width=800,
height=480,
de_pin=42,
hsync_pin=40,
vsync_pin=41,
pclk_pin=39,
data_pins={
"red": [7, 17, 18, 3, 46],
"green": [9, 10, 11, 12, 13, 14],
"blue": [21, 47, 48, 45, 38],
},
)
+2 -1
View File
@@ -25,7 +25,8 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
" SPI Bus width: %d",
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(has_hardware_rotation), YESNO(invert_colors),
(madctl & MADCTL_BGR) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode,
(madctl & MADCTL_BGR) ? LOG_STR_LITERAL("BGR") : LOG_STR_LITERAL("RGB"), display_bits,
is_big_endian ? LOG_STR_LITERAL("Big") : LOG_STR_LITERAL("Little"), spi_mode,
static_cast<unsigned>(data_rate / 1000000), bus_width);
LOG_PIN(" CS Pin: ", cs);
LOG_PIN(" Reset Pin: ", reset);
@@ -0,0 +1,69 @@
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TAG
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@FredM67"]
DEPENDENCIES = ["uart"]
mk2pvrouter_ns = cg.esphome_ns.namespace("mk2pvrouter")
Mk2PVRouter = mk2pvrouter_ns.class_("Mk2PVRouter", cg.Component, uart.UARTDevice)
CONF_MK2PVROUTER_ID = "mk2pvrouter_id"
# Tags are copied into a fixed-size buffer (MAX_TAG_SIZE = 8 in mk2pvrouter.h),
# which needs room for a trailing null terminator.
MAX_TAG_LEN = 7
MK2PVROUTER_LISTENER_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_MK2PVROUTER_ID): cv.use_id(Mk2PVRouter),
cv.Required(CONF_TAG): cv.All(
cv.string_strict, cv.Length(min=1, max=MAX_TAG_LEN), lambda x: x.upper()
),
}
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Mk2PVRouter),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
def final_validate(config: ConfigType) -> None:
# Validate UART settings
schema = uart.final_validate_device_schema(
"mk2pvrouter",
baud_rate=9600,
parity="EVEN",
data_bits=7,
stop_bits=1,
require_rx=True,
require_tx=False,
)
schema(config)
FINAL_VALIDATE_SCHEMA = final_validate
_request_listener_slot = cg.slot_counter("MK2PVROUTER_LISTENER_COUNT")
async def register_mk2pvrouter_listener(mk2pvrouter: MockObj, var: MockObj) -> None:
"""Register a listener with its hub and count it for the compile-time buffer size."""
_request_listener_slot()
cg.add(mk2pvrouter.register_mk2pvrouter_listener(var))
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
@@ -0,0 +1,177 @@
#include "mk2pvrouter.h"
#include "esphome/core/log.h"
#include <cstring>
namespace esphome::mk2pvrouter {
static const char *const TAG = "mk2pvrouter";
constexpr uint8_t START_FRAME = 0x2;
constexpr uint8_t END_FRAME = 0x3;
constexpr uint8_t LINE_FEED = 0xa;
constexpr uint8_t CARRIAGE_RETURN = 0xd;
constexpr uint8_t TAB = 0x9;
constexpr uint8_t MAX_ITERATIONS = 128;
constexpr uint8_t CRC_MASK = 0x3F;
constexpr uint8_t CRC_OFFSET = 0x20;
// Extracts a TAB-delimited field from [buf_start, buf_end) into dest.
// Returns the field length, or 0 if no TAB was found, or the (uncopied) field
// length if it's >= max_len.
static size_t get_field(char *dest, const char *buf_start, const char *buf_end, size_t max_len) {
const auto *const field_end = static_cast<const char *>(memchr(buf_start, TAB, buf_end - buf_start));
if (!field_end)
return 0;
const size_t len = field_end - buf_start;
if (len >= max_len) {
ESP_LOGE(TAG, "Field too long: %zu bytes (max %zu)", len, max_len);
return len;
}
memcpy(dest, buf_start, len);
dest[len] = '\0'; // Null-terminate
return len;
}
// Calculates the CRC (checksum) for a given group of characters.
uint8_t Mk2PVRouter::calculate_crc_(const char *grp, size_t grp_len) {
uint8_t crc_tmp{0};
const auto effective_len = grp_len - CRC_SUFFIX_LEN;
for (size_t i = 0; i < effective_len; i++) {
crc_tmp += grp[i];
}
crc_tmp &= CRC_MASK;
crc_tmp += CRC_OFFSET;
return crc_tmp;
}
// Verifies the CRC of a group against its trailing CRC byte.
bool Mk2PVRouter::check_crc_(const char *grp, const char *grp_end) {
const auto grp_len = grp_end - grp;
if (grp_len < static_cast<decltype(grp_len)>(CRC_SUFFIX_LEN)) {
ESP_LOGE(TAG, "Empty or too short group");
return false;
}
const auto raw_crc = grp[grp_len - 1];
const auto calculated_crc = this->calculate_crc_(grp, grp_len);
if (raw_crc != calculated_crc) {
ESP_LOGE(TAG, "CRC mismatch: expected %d, got %d", calculated_crc, raw_crc);
return false;
}
return true;
}
// Validates, parses, and publishes a single tag/value group.
void Mk2PVRouter::process_group_(const char *grp, const char *grp_end) {
if (!this->check_crc_(grp, grp_end))
return;
size_t field_len = get_field(this->tag_, grp, grp_end, MAX_TAG_SIZE);
if (!field_len || field_len >= MAX_TAG_SIZE) {
ESP_LOGE(TAG, "Invalid tag");
return;
}
const auto *val_start = grp + field_len + 1; // Skip tag + TAB.
field_len = get_field(this->val_, val_start, grp_end, MAX_VAL_SIZE);
if (!field_len || field_len >= MAX_VAL_SIZE) {
ESP_LOGE(TAG, "Invalid value for tag %s", this->tag_);
return;
}
this->publish_value_(this->tag_, this->val_);
}
// Reads characters until `c` is found or the internal buffer is full.
bool Mk2PVRouter::read_chars_until_(bool drop, uint8_t c) {
size_t j{0};
while (this->available() > 0 && j++ < MAX_ITERATIONS) {
const auto received = this->read();
if (received < 0)
continue;
if (received == c)
return true;
if (drop)
continue;
if (this->buf_index_ >= (sizeof(this->buf_) - 1)) {
ESP_LOGW(TAG, "Internal buffer full");
this->buf_index_ = 0;
this->state_ = State::WAITING_FOR_START;
return false;
}
this->buf_[this->buf_index_++] = received;
}
return false;
}
void Mk2PVRouter::loop() {
switch (this->state_) {
case State::WAITING_FOR_START:
ESP_LOGVV(TAG, "State: WAITING_FOR_START");
if (this->read_chars_until_(true, START_FRAME))
this->state_ = State::START_FRAME_RECEIVED;
break;
case State::START_FRAME_RECEIVED:
ESP_LOGVV(TAG, "State: START_FRAME_RECEIVED");
if (this->read_chars_until_(false, END_FRAME))
this->state_ = State::END_FRAME_RECEIVED;
break;
case State::END_FRAME_RECEIVED: {
ESP_LOGVV(TAG, "State: END_FRAME_RECEIVED -> processing");
if (this->buf_index_ == 0) {
this->state_ = State::WAITING_FOR_START;
break;
}
auto *buf_finger = this->buf_;
auto *buf_end = this->buf_ + this->buf_index_;
// Each group: 0xa(LF) | Tag | 0x9(TAB) | Data | 0x9(TAB) | CRC | 0xd(CR)
// CRC is computed over "Tag | TAB | Data | TAB".
while ((buf_finger = static_cast<char *>(memchr(buf_finger, LINE_FEED, buf_end - buf_finger))) != nullptr) {
++buf_finger; // Skip LF to the start of the group.
auto *const grp_end = static_cast<char *>(memchr(buf_finger, CARRIAGE_RETURN, buf_end - buf_finger));
if (!grp_end) {
ESP_LOGE(TAG, "No group found");
break;
}
this->process_group_(buf_finger, grp_end);
buf_finger = grp_end; // grp_end is always < buf_end, so this stays in bounds.
}
this->buf_index_ = 0;
this->state_ = State::WAITING_FOR_START;
break;
}
}
}
void Mk2PVRouter::publish_value_(const char *tag, const char *val) {
#ifdef MK2PVROUTER_LISTENER_COUNT
for (auto *element : this->mk2pvrouter_listeners_) {
if (strcmp(tag, element->get_tag()) != 0)
continue;
element->publish_val(val);
}
#endif
}
void Mk2PVRouter::dump_config() {
ESP_LOGCONFIG(TAG, "Mk2PVRouter:");
this->check_uart_settings(BAUD_RATE, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
}
#ifdef MK2PVROUTER_LISTENER_COUNT
void Mk2PVRouter::register_mk2pvrouter_listener(Mk2PVRouterListener *listener) {
this->mk2pvrouter_listeners_.push_back(listener);
}
#endif
} // namespace esphome::mk2pvrouter
@@ -0,0 +1,69 @@
#pragma once
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
namespace esphome::mk2pvrouter {
/*
* Buffer sizes based on the mk2pvrouter telemetry protocol, as implemented by the
* firmware's teleinfo.h (see github.com/FredM67/PVRouter-{1,3}-phase):
* - Tags: max 4 chars (S_MC is longest), most are 1-2 chars (P, V1, R2, etc.)
* - Values: max 6 digits signed (-10000), typical 1-5 digits. Energy (E) is a daily
* counter reset at midnight, so it stays well within 6 digits.
* - Frame: STX + multiple lines (LF+tag+TAB+value+TAB+crc+CR) + ETX
* - Line format: \n<tag>\t<value>\t<crc>\r (8-15 bytes per line)
* - Multi-phase with all features: ~150-200 bytes
*/
static constexpr uint8_t MAX_TAG_SIZE = 8; // S_MC (4) + digit (1) + null (1) + margin (2)
static constexpr uint8_t MAX_VAL_SIZE = 8; // -10000 (6) + null (1) + margin (1)
static constexpr uint16_t MAX_BUF_SIZE = 256; // Full frame with all features enabled
// Listener interface for entities that want updates for a specific tag.
class Mk2PVRouterListener {
public:
explicit Mk2PVRouterListener(const char *tag) : tag_(tag) {}
virtual ~Mk2PVRouterListener() = default;
const char *get_tag() const { return this->tag_; }
virtual void publish_val(const char *val) = 0;
protected:
const char *tag_;
};
// Reads frames via UART, validates their CRC, and publishes tag/value pairs to listeners.
class Mk2PVRouter final : public Component, public uart::UARTDevice {
public:
#ifdef MK2PVROUTER_LISTENER_COUNT
void register_mk2pvrouter_listener(Mk2PVRouterListener *listener);
#endif
void loop() override;
void dump_config() override;
protected:
static constexpr size_t CRC_SUFFIX_LEN = 1;
static constexpr uint32_t BAUD_RATE = 9600;
enum class State : uint8_t {
WAITING_FOR_START,
START_FRAME_RECEIVED,
END_FRAME_RECEIVED,
};
#ifdef MK2PVROUTER_LISTENER_COUNT
StaticVector<Mk2PVRouterListener *, MK2PVROUTER_LISTENER_COUNT> mk2pvrouter_listeners_;
#endif
uint16_t buf_index_{0};
State state_{State::WAITING_FOR_START};
char tag_[MAX_TAG_SIZE];
char val_[MAX_VAL_SIZE];
char buf_[MAX_BUF_SIZE]; // Large buffer last to reduce padding
bool read_chars_until_(bool drop, uint8_t c);
uint8_t calculate_crc_(const char *grp, size_t grp_len);
bool check_crc_(const char *grp, const char *grp_end);
void process_group_(const char *grp, const char *grp_end);
void publish_value_(const char *tag, const char *val);
};
} // namespace esphome::mk2pvrouter
@@ -0,0 +1,27 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.const import CONF_ID, CONF_TAG
from esphome.types import ConfigType
from .. import (
CONF_MK2PVROUTER_ID,
MK2PVROUTER_LISTENER_SCHEMA,
mk2pvrouter_ns,
register_mk2pvrouter_listener,
)
Mk2PVRouterSensor = mk2pvrouter_ns.class_(
"Mk2PVRouterSensor", sensor.Sensor, cg.Component
)
CONFIG_SCHEMA = sensor.sensor_schema(Mk2PVRouterSensor).extend(
MK2PVROUTER_LISTENER_SCHEMA
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
mk2pvrouter = await cg.get_variable(config[CONF_MK2PVROUTER_ID])
await register_mk2pvrouter_listener(mk2pvrouter, var)
@@ -0,0 +1,24 @@
#include "mk2pvrouter_sensor.h"
#include "esphome/core/log.h"
namespace esphome::mk2pvrouter {
static const char *const TAG = "mk2pvrouter_sensor";
Mk2PVRouterSensor::Mk2PVRouterSensor(const char *tag) : Mk2PVRouterListener(tag) {}
void Mk2PVRouterSensor::publish_val(const char *val) {
auto result = parse_number<float>(val);
if (!result.has_value()) {
ESP_LOGW(TAG, "Failed to parse value '%s' for tag '%s'", val, this->get_tag());
return;
}
this->publish_state(result.value());
}
void Mk2PVRouterSensor::dump_config() {
LOG_SENSOR(" ", "Mk2PVRouter Sensor", this);
ESP_LOGCONFIG(TAG, " Tag: %s", this->get_tag());
}
} // namespace esphome::mk2pvrouter
@@ -0,0 +1,15 @@
#pragma once
#include "esphome/components/mk2pvrouter/mk2pvrouter.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::mk2pvrouter {
class Mk2PVRouterSensor final : public Mk2PVRouterListener, public sensor::Sensor, public Component {
public:
explicit Mk2PVRouterSensor(const char *tag);
void publish_val(const char *val) override;
void dump_config() override;
};
} // namespace esphome::mk2pvrouter
+121 -65
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -12,29 +13,49 @@ static const char *const TAG = "modbus";
static constexpr size_t MODBUS_MAX_LOG_BYTES = 64;
// Approximate bits per character on the wire (depends on parity/stop bit config)
static constexpr uint32_t MODBUS_BITS_PER_CHAR = 11;
static constexpr uint32_t MS_PER_SEC = 1000;
static constexpr uint32_t US_PER_SEC = 1000000;
static constexpr uint32_t US_PER_MS = 1000;
// Minimum interframe delay per the Modbus spec (fixed 1750us above 19200 baud)
static constexpr uint32_t MODBUS_MIN_FRAME_DELAY_US = 1750;
// Diagnostics only: the backdated byte stamp can precede last_send_ (echo, or noise during our own
// send), where an unsigned wrap would print ~4.29e9.
static uint32_t us_since_send(uint32_t last_modbus_byte, uint32_t last_send) {
const uint32_t elapsed = last_modbus_byte - last_send;
return (int32_t) elapsed < 0 ? 0 : elapsed;
}
void Modbus::setup() {
if (this->flow_control_pin_ != nullptr) {
this->flow_control_pin_->setup();
}
this->frame_delay_ms_ =
std::max(2, // 1750us minimum per spec - rounded up to 2ms.
// 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay)
(uint16_t) (3.5 * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1);
// RTU specifies 11 bits per character but 8N1 is 10, so derive it from the framing. The schema
// forbids a zero, so one here means the hub never set it (weikai): fall back to 8N1 and a 1 baud floor.
const uint8_t data_bits = this->parent_->get_data_bits() != 0 ? this->parent_->get_data_bits() : 8;
const uint8_t stop_bits = this->parent_->get_stop_bits() != 0 ? this->parent_->get_stop_bits() : 1;
const uint32_t baud_rate = std::max<uint32_t>(1u, this->parent_->get_baud_rate());
this->bits_per_char_ = static_cast<uint8_t>(
1 + data_bits + (this->parent_->get_parity() == uart::UART_CONFIG_PARITY_NONE ? 0 : 1) + stop_bits);
// 3.5 characters * bits per character * 1e6 us/sec / (bits/sec) (Standard modbus frame delay)
this->frame_delay_us_ =
std::max(MODBUS_MIN_FRAME_DELAY_US, (uint32_t) (3.5 * this->bits_per_char_ * US_PER_SEC / baud_rate) + 1);
// When rx_full_threshold is configured (non-zero), the UART has a hardware FIFO with a
// meaningful threshold (e.g., ESP32 native UART), so we can calculate a precise delay.
// Otherwise (e.g., USB UART), use 50ms to handle data arriving in chunks.
static constexpr uint16_t DEFAULT_LONG_RX_BUFFER_DELAY_MS = 50;
static constexpr uint32_t DEFAULT_LONG_RX_BUFFER_DELAY_US = 50 * US_PER_MS;
size_t rx_threshold = this->parent_->get_rx_full_threshold();
this->long_rx_buffer_delay_ms_ =
rx_threshold != uart::UARTComponent::RX_FULL_THRESHOLD_UNSET
? (rx_threshold * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1
: DEFAULT_LONG_RX_BUFFER_DELAY_MS;
this->long_rx_buffer_delay_us_ = rx_threshold != uart::UARTComponent::RX_FULL_THRESHOLD_UNSET
? (uint32_t) (rx_threshold * this->bits_per_char_ * US_PER_SEC / baud_rate) + 1
: DEFAULT_LONG_RX_BUFFER_DELAY_US;
// The idle-timeout interrupt fires rx_timeout characters after the last byte, so that much silence
// has already passed by the time we read it: backdate so the gap measures silence on the wire.
this->rx_detect_latency_us_ =
(uint32_t) (this->parent_->get_rx_timeout() * this->bits_per_char_ * US_PER_SEC / baud_rate);
}
void Modbus::loop() {
@@ -52,7 +73,7 @@ void ModbusClientHub::loop() {
// Send-wait watchdog: only the cheap time check runs at loop rate; expire_waiting_() looks the
// entry up and holds off if the response has started arriving.
if (this->waiting_for_response_ &&
this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_) {
this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_us_) {
this->expire_waiting_();
}
@@ -72,7 +93,7 @@ void ModbusClientHub::expire_waiting_() {
}
// Only a genuine WAITING entry warrants the log (a cleared or interrupted shell timing out is expected).
if (cmd->state == FrameState::WAITING) {
ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", cmd->frame.address(),
ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "us after last send", cmd->frame.address(),
this->last_receive_check_ - this->last_send_);
}
// Deliver on_no_response directly, the way the parse path delivers response()/error(): the entry
@@ -86,37 +107,47 @@ void ModbusClientHub::expire_waiting_() {
bool Modbus::timeout_() {
// If the response frame is finished (including interframe delay) - we timeout.
// The long_rx_buffer_delay accounts for long responses (larger than the UART rx_full_threshold) to avoid timeouts
// when the buffer is filling the back half of the response
const uint16_t timeout = std::max(
(uint16_t) this->frame_delay_ms_,
(uint16_t) (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold() ? this->long_rx_buffer_delay_ms_
: 0));
// when the buffer is filling the back half of the response. The latch decides, not the current size:
// parsing a leading frame can shrink the buffer below the threshold while the rest is still streaming.
// The latency term covers the final batch, which is idle-delivered.
const uint32_t timeout =
this->exceeded_rx_full_threshold_
? std::max(this->frame_delay_us_, this->long_rx_buffer_delay_us_ + this->rx_detect_latency_us_)
: this->frame_delay_us_;
return this->last_receive_check_ - this->last_modbus_byte_ > timeout;
}
// We use micros() here and elsewhere instead of App.get_loop_component_start_time() to avoid stale timestamps
// It's critical in all timestamp comparisons that the left timestamp comes before the right one in time
// If we use a cached value in place of micros() and last_modbus_byte_ is updated inside our loop
// then the comparison is backwards (small negative which wraps to large positive) and will cause a false timeout
// So in this component we don't use any cached timestamp values to avoid these annoying bugs.
// Compare before subtracting: a signed difference would read a bus idle past half the micros() wrap
// (~35 min) as a huge delay still owed.
static inline uint32_t remaining_delay(uint32_t elapsed, uint32_t required) {
return elapsed >= required ? 0 : required - elapsed;
}
int32_t Modbus::tx_delay_remaining() {
// millis() here and everywhere in this component, never a cached loop timestamp: a cached "now" can
// predate last_modbus_byte_, and the unsigned subtraction then wraps huge and forces a false timeout.
const uint32_t now = millis();
return std::max({(int32_t) 0,
(int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ - (now - this->last_send_)),
(int32_t) (this->frame_delay_ms_ - (now - this->last_modbus_byte_))});
const uint32_t now = micros();
return (int32_t) std::max(remaining_delay(now - this->last_send_, this->last_send_tx_offset_ + this->frame_delay_us_),
remaining_delay(now - this->last_modbus_byte_, this->frame_delay_us_));
}
int32_t ModbusClientHub::tx_delay_remaining() {
const uint32_t now = millis();
return std::max({(int32_t) 0,
(int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + this->turnaround_delay_ms_ -
(now - this->last_send_)),
(int32_t) (this->frame_delay_ms_ + this->turnaround_delay_ms_ - (now - this->last_modbus_byte_))});
const uint32_t now = micros();
return (int32_t) std::max(
remaining_delay(now - this->last_send_,
this->last_send_tx_offset_ + this->frame_delay_us_ + this->turnaround_delay_us_),
remaining_delay(now - this->last_modbus_byte_, this->frame_delay_us_ + this->turnaround_delay_us_));
}
bool Modbus::tx_blocked() {
// Blocked while any rx bytes are pending, or within tx_delay of the last byte in either direction
// (receivers must see our previous tx as done, and more rx may be coming). A remaining delay up to
// MODBUS_TX_MAX_DELAY_MS doesn't block - send_frame_ absorbs it instead of looping on small waits.
return this->available() || !this->rx_buffer_.empty() || this->tx_delay_remaining() > MODBUS_TX_MAX_DELAY_MS;
// MODBUS_TX_MAX_DELAY_US doesn't block - send_frame_ absorbs it instead of looping on small waits.
return this->available() || !this->rx_buffer_.empty() || this->tx_delay_remaining() > MODBUS_TX_MAX_DELAY_US;
}
bool ModbusClientHub::tx_blocked() { return this->waiting_for_response_ || this->Modbus::tx_blocked(); }
@@ -133,20 +164,26 @@ bool ModbusClientHub::tx_buffer_empty() {
}
void Modbus::receive_bytes_() {
this->last_receive_check_ = millis();
this->last_receive_check_ = micros();
size_t bytes = this->available();
if (bytes) {
size_t buffer_size = this->rx_buffer_.size();
this->last_modbus_byte_ = this->last_receive_check_;
// Below the threshold the batch can only be idle-delivered, so its last byte finished one detection
// latency ago; at or above it the frame may still be streaming, so stamp now.
this->last_modbus_byte_ = bytes < this->parent_->get_rx_full_threshold()
? this->last_receive_check_ - this->rx_detect_latency_us_
: this->last_receive_check_;
this->rx_buffer_.resize(buffer_size + bytes);
if (!this->read_array(this->rx_buffer_.data() + buffer_size, bytes)) {
this->rx_buffer_.resize(buffer_size);
return;
}
if (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold())
this->exceeded_rx_full_threshold_ = true;
if (buffer_size == 0) {
ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) of %zu bytes %" PRIu32 "ms after last send",
this->rx_buffer_[0], this->rx_buffer_[0], this->rx_buffer_.size(), millis() - this->last_send_);
ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) of %zu bytes %" PRIu32 "us after last send",
this->rx_buffer_[0], this->rx_buffer_[0], this->rx_buffer_.size(), micros() - this->last_send_);
}
}
}
@@ -299,8 +336,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span<con
ModbusDeviceCommand *cmd = this->waiting_for_response_ ? this->find_waiting_() : nullptr;
if (cmd == nullptr) {
ESP_LOGW(TAG,
"Received unexpected frame from address %" PRIu8 ", function code 0x%X, %" PRIu32 "ms after last send",
address, function_code, this->last_modbus_byte_ - this->last_send_);
"Received unexpected frame from address %" PRIu8 ", function code 0x%X, %" PRIu32 "us after last send",
address, function_code, us_since_send(this->last_modbus_byte_, this->last_send_));
return;
}
@@ -310,9 +347,9 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span<con
if (expected_address != address || expected_function_code != (function_code & FUNCTION_CODE_MASK)) {
ESP_LOGW(TAG,
"Received incorrect frame address %" PRIu8 " <> %" PRIu8 " or function code 0x%X <> 0x%X, %" PRIu32
"ms after last send",
"us after last send",
address, expected_address, (function_code & FUNCTION_CODE_MASK), expected_function_code,
this->last_modbus_byte_ - this->last_send_);
us_since_send(this->last_modbus_byte_, this->last_send_));
// Unexpected frame: flip a WAITING entry to an INTERRUPTED shell that ignores the rest of this
// transaction and blocks tx until the send-wait timeout, where it gets its on_no_response.
cmd->interrupt();
@@ -325,8 +362,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span<con
// cleared-interrupted frame still ends in on_no_response rather than delivering a late response.
ESP_LOGW(TAG,
"Ignoring response from %" PRIu8 " - transmission interrupted by previous unexpected response, %" PRIu32
"ms after last send",
address, this->last_modbus_byte_ - this->last_send_);
"us after last send",
address, us_since_send(this->last_modbus_byte_, this->last_send_));
return;
}
@@ -337,12 +374,12 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span<con
this->sweep_needed_ = true;
if (helpers::is_function_code_exception(function_code)) {
uint8_t exception = pdu[1]; // exception frames are fixed-length, so the code is always present
ESP_LOGW(TAG, "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "ms after last send",
function_code, exception, address, this->last_modbus_byte_ - this->last_send_);
ESP_LOGW(TAG, "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "us after last send",
function_code, exception, address, us_since_send(this->last_modbus_byte_, this->last_send_));
cmd->error(static_cast<ExceptionCode>(exception));
} else if (!cmd->response(pdu)) {
ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "ms after last send", address,
this->last_modbus_byte_ - this->last_send_);
ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "us after last send", address,
us_since_send(this->last_modbus_byte_, this->last_send_));
}
}
@@ -738,9 +775,16 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func
// Callers gate on tx_blocked() first, but the pre-send delay below can span several ms, so re-check
// after it and refuse (return false) if a byte arrived in that window rather than transmit over it.
bool Modbus::send_frame_(const ModbusFrame &frame) {
const int32_t tx_delay_remaining = this->tx_delay_remaining();
int32_t tx_delay_remaining = this->tx_delay_remaining();
if (tx_delay_remaining > 0) {
delay(tx_delay_remaining);
// Yield the whole-ms part: delay() never blocks past the request on FreeRTOS, and only slightly
// over elsewhere, which just lengthens the gap. The recompute below makes the remainder exact.
if (tx_delay_remaining >= (int32_t) US_PER_MS) {
delay(tx_delay_remaining / US_PER_MS);
tx_delay_remaining = this->tx_delay_remaining();
}
if (tx_delay_remaining > 0)
delayMicroseconds(tx_delay_remaining);
}
if (this->tx_blocked()) {
@@ -755,14 +799,15 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
this->last_send_tx_offset_ = 0;
} else {
this->write_array(frame.data.data(), frame.size());
this->last_send_tx_offset_ = frame.size() * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1;
this->last_send_tx_offset_ =
frame.size() * this->bits_per_char_ * US_PER_SEC / std::max<uint32_t>(1u, this->parent_->get_baud_rate()) + 1;
}
uint32_t now = millis();
uint32_t now = micros();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send, %" PRIu32 "ms after last receive",
ESP_LOGV(TAG, "Write: %s %" PRIu32 "us after last send, %" PRIu32 "us after last receive",
format_hex_pretty_to(hex_buf, frame.data.data(), frame.size()), now - this->last_send_,
now - this->last_modbus_byte_);
this->last_send_ = now;
@@ -770,6 +815,9 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
}
void ModbusClientHub::send_next_frame_() {
if (this->tx_buffer_.empty())
return;
if (this->tx_blocked())
return;
@@ -800,20 +848,25 @@ void ModbusClientHub::send_next_frame_() {
void ModbusClientHub::dump_config() {
ESP_LOGCONFIG(TAG,
"Modbus:\n"
" Send Wait Time: %" PRIu16 " ms\n"
" Turnaround Time: %" PRIu16 " ms\n"
" Frame Delay: %" PRIu16 " ms\n"
" Long Rx Buffer Delay: %" PRIu16 " ms",
this->send_wait_time_, this->turnaround_delay_ms_, this->frame_delay_ms_,
this->long_rx_buffer_delay_ms_);
" Send Wait Time: %" PRIu32 " ms\n"
" Turnaround Time: %" PRIu32 " ms\n"
" Frame Delay: %" PRIu32 " us\n"
" Long Rx Buffer Delay: %" PRIu32 " us\n"
" Bits Per Character: %" PRIu8 "\n"
" Rx Detect Latency: %" PRIu32 " us",
this->send_wait_time_us_ / US_PER_MS, this->turnaround_delay_us_ / US_PER_MS, this->frame_delay_us_,
this->long_rx_buffer_delay_us_, this->bits_per_char_, this->rx_detect_latency_us_);
LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
}
void ModbusServerHub::dump_config() {
ESP_LOGCONFIG(TAG,
"Modbus:\n"
" Frame Delay: %" PRIu16 " ms\n"
" Long Rx Buffer Delay: %" PRIu16 " ms",
this->frame_delay_ms_, this->long_rx_buffer_delay_ms_);
" Frame Delay: %" PRIu32 " us\n"
" Long Rx Buffer Delay: %" PRIu32 " us\n"
" Bits Per Character: %" PRIu8 "\n"
" Rx Detect Latency: %" PRIu32 " us",
this->frame_delay_us_, this->long_rx_buffer_delay_us_, this->bits_per_char_,
this->rx_detect_latency_us_);
LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
}
@@ -1142,7 +1195,8 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) {
// without a heap allocation. Only one server reply is ever waiting, so a single buffer suffices.
std::memcpy(this->deferred_payload_.data(), payload, len);
this->deferred_payload_len_ = len;
this->set_timeout("deferred_send", this->tx_delay_remaining(), [this]() {
// set_timeout() takes milliseconds; round the microsecond delay up so we never fire early.
this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() {
ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1,
this->deferred_payload_len_ - 1);
if (!this->send_frame_(frame))
@@ -1162,11 +1216,11 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_t
bytes = bytes_to_clear;
if (bytes > 0) {
if (warn) {
ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", bytes, LOG_STR_ARG(reason),
millis() - this->last_send_);
ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "us after last send", bytes, LOG_STR_ARG(reason),
micros() - this->last_send_);
} else {
ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", bytes, LOG_STR_ARG(reason),
millis() - this->last_send_);
ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "us after last send", bytes, LOG_STR_ARG(reason),
micros() - this->last_send_);
}
if (bytes == this->rx_buffer_.size()) {
this->rx_buffer_.clear();
@@ -1174,6 +1228,8 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_t
this->rx_buffer_.erase(this->rx_buffer_.begin(), this->rx_buffer_.begin() + bytes);
}
}
if (this->rx_buffer_.empty())
this->exceeded_rx_full_threshold_ = false;
}
void ModbusClientDevice::dispatch_response_(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
+14 -7
View File
@@ -19,7 +19,7 @@ namespace esphome::modbus {
// Tx queue backstop: duplicates dedup into one entry, so only a runaway generator of distinct frames
// (e.g. a loop writing a changing value) could grow the heap unboundedly.
static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 128;
static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5;
static constexpr uint16_t MODBUS_TX_MAX_DELAY_US = 5000;
// Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes
// (address + 5-byte PDU + 2-byte CRC).
@@ -70,12 +70,18 @@ class Modbus : public uart::UARTDevice, public Component {
bool send_frame_(const ModbusFrame &frame);
uint16_t find_frame_end_by_crc_(uint16_t min_length) const;
// All timestamps and durations below are micros()-based
uint32_t last_modbus_byte_{0};
uint32_t last_receive_check_{0};
uint32_t last_send_{0};
uint32_t last_send_tx_offset_{0};
uint16_t frame_delay_ms_{5};
uint16_t long_rx_buffer_delay_ms_{0};
uint32_t frame_delay_us_{5000};
uint32_t long_rx_buffer_delay_us_{0};
uint32_t rx_detect_latency_us_{0};
// Bits on the wire per character (start + data + optional parity + stop); 12 at most.
uint8_t bits_per_char_{11};
// Latched when a read reaches rx_full_threshold, cleared when the buffer drains.
bool exceeded_rx_full_threshold_{false};
GPIOPin *flow_control_pin_{nullptr};
@@ -232,8 +238,9 @@ class ModbusClientHub : public Modbus {
ModbusClientHub() = default;
void dump_config() override;
void loop() override;
void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_ = time_in_ms; }
void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_ms_ = time_in_ms; }
// Config arrives in milliseconds; stored internally in microseconds like all other timing.
void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_us_ = time_in_ms * 1000UL; }
void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_us_ = time_in_ms * 1000UL; }
bool tx_buffer_empty();
bool tx_blocked() override;
ESPDEPRECATED("Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0")
@@ -279,8 +286,8 @@ class ModbusClientHub : public Modbus {
// End the wait for a response on send-wait timeout (the loop() watchdog body); see FrameState.
void expire_waiting_();
uint16_t send_wait_time_{2000};
uint16_t turnaround_delay_ms_{0};
uint32_t send_wait_time_us_{2000000};
uint32_t turnaround_delay_us_{0};
// Set on transmit, cleared on the transaction-ending transition; send_next_frame_ won't select
// while it is set, so at most one frame is awaiting a response.
+41 -14
View File
@@ -292,25 +292,52 @@ std::optional<int64_t> payload_to_number(const uint8_t *data, size_t size, Senso
}
std::optional<int64_t> registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type) {
const size_t required_size = required_payload_size(sensor_value_type);
if (required_size == 0) {
return 0; // RAW/unsupported: nothing to read
// RAW and BIT carry no fixed-width number, so there is nothing to decode whatever the span holds.
// register_width_for() reports 1 for them, so this must be checked before the width test below.
if (sensor_value_type == SensorValueType::RAW || sensor_value_type == SensorValueType::BIT) {
return 0;
}
const size_t required_words = required_size / 2;
const uint16_t required_words = register_width_for(sensor_value_type);
if (required_words > count) {
ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%zu",
static_cast<unsigned int>(sensor_value_type), count, required_words);
ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%u",
static_cast<unsigned int>(sensor_value_type), count, static_cast<unsigned int>(required_words));
return std::nullopt;
}
// Serialize the needed words back to big-endian bytes and reuse the audited byte decoder so the
// sign-extension behaviour stays identical to the wire path.
uint8_t bytes[8]; // at most 4 registers (QWORD)
for (size_t i = 0; i < required_words; i++) {
uint16_t reg = registers[i];
bytes[i * 2] = static_cast<uint8_t>(reg >> 8);
bytes[i * 2 + 1] = static_cast<uint8_t>(reg & 0xFF);
// Registers are the wire's own unit, so decode them directly rather than serializing back to bytes.
// Each case defers to registers_to_value() so the word order and sign rules have one definition, with
// two deliberate exceptions matching what the byte decoder returned: the float types yield their bit
// pattern rather than a float, and U_QWORD shares the signed branch because the return type is int64_t.
switch (sensor_value_type) {
case SensorValueType::U_WORD:
return registers_to_value<SensorValueType::U_WORD>(registers);
case SensorValueType::U_WORD_S:
return registers_to_value<SensorValueType::U_WORD_S>(registers);
case SensorValueType::S_WORD:
return registers_to_value<SensorValueType::S_WORD>(registers);
case SensorValueType::S_WORD_S:
return registers_to_value<SensorValueType::S_WORD_S>(registers);
case SensorValueType::U_DWORD:
return registers_to_value<SensorValueType::U_DWORD>(registers);
case SensorValueType::U_DWORD_R:
return registers_to_value<SensorValueType::U_DWORD_R>(registers);
case SensorValueType::S_DWORD:
return registers_to_value<SensorValueType::S_DWORD>(registers);
case SensorValueType::S_DWORD_R:
return registers_to_value<SensorValueType::S_DWORD_R>(registers);
case SensorValueType::FP32:
return registers_to_uint32(registers[0], registers[1]);
case SensorValueType::FP32_R:
return registers_to_uint32(registers[1], registers[0]);
// Signed for both: an unsigned QWORD above INT64_MAX has to come back as a negative int64_t.
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
return registers_to_value<SensorValueType::S_QWORD>(registers);
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R:
return registers_to_value<SensorValueType::S_QWORD_R>(registers);
default:
return 0;
}
return payload_to_number(bytes, required_size, sensor_value_type, 0, 0xFFFFFFFF);
}
// Append a 16-bit value to a PDU in big-endian (wire) byte order.
+82 -1
View File
@@ -229,7 +229,7 @@ inline bool value_type_is_float(SensorValueType v) {
}
/// Number of 16-bit registers a value of this type occupies (RAW counts as one register).
inline uint16_t register_width_for(SensorValueType v) {
constexpr uint16_t register_width_for(SensorValueType v) {
switch (v) {
case SensorValueType::U_DWORD:
case SensorValueType::S_DWORD:
@@ -473,6 +473,87 @@ inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueTy
*/
std::optional<int64_t> registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type);
/// Combine two register words into a 32-bit value.
constexpr uint32_t registers_to_uint32(uint16_t high_word, uint16_t low_word) {
return (static_cast<uint32_t>(high_word) << 16) | low_word;
}
/// Combine four register words into a 64-bit value, most significant word first.
constexpr uint64_t registers_to_uint64(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) {
return (static_cast<uint64_t>(registers_to_uint32(word0, word1)) << 32) | registers_to_uint32(word2, word3);
}
// Always false, whatever the type: it exists only to make the static_assert below depend on the
// template argument. Not a queryable trait.
template<SensorValueType> inline constexpr bool VALUE_TYPE_SUPPORTED = false;
/** Decode one value whose type is known at compile time, from registers in host byte order.
* Unlike registers_to_number(), the type is a template argument, so only the one decode is compiled
* and the caller gets the value's natural type back rather than an int64_t. The "_R" types take the
* low word first; the rest take the high word first.
* Supports every fixed-width type: the WORD, DWORD, QWORD and FP32 families, including their _S and
* _R forms. RAW and BIT have no fixed width and fail to compile.
* Use register_width_for() for the number of registers the caller must supply.
* Note that the FP32 branches are only usable in a constant expression where std::bit_cast is
* available; elsewhere bit_cast falls back to a non-constexpr memcpy (see core/helpers.h).
*/
template<SensorValueType VALUE_TYPE> constexpr auto registers_to_value(const uint16_t *registers) {
if constexpr (VALUE_TYPE == SensorValueType::U_WORD) {
return registers[0];
} else if constexpr (VALUE_TYPE == SensorValueType::S_WORD) {
return static_cast<int16_t>(registers[0]);
} else if constexpr (VALUE_TYPE == SensorValueType::U_WORD_S) {
return byteswap(registers[0]);
} else if constexpr (VALUE_TYPE == SensorValueType::S_WORD_S) {
return static_cast<int16_t>(byteswap(registers[0]));
} else if constexpr (VALUE_TYPE == SensorValueType::U_DWORD) {
return registers_to_uint32(registers[0], registers[1]);
} else if constexpr (VALUE_TYPE == SensorValueType::U_DWORD_R) {
return registers_to_uint32(registers[1], registers[0]);
} else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD) {
return static_cast<int32_t>(registers_to_uint32(registers[0], registers[1]));
} else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD_R) {
return static_cast<int32_t>(registers_to_uint32(registers[1], registers[0]));
} else if constexpr (VALUE_TYPE == SensorValueType::FP32) {
return bit_cast<float>(registers_to_uint32(registers[0], registers[1]));
} else if constexpr (VALUE_TYPE == SensorValueType::FP32_R) {
return bit_cast<float>(registers_to_uint32(registers[1], registers[0]));
} else if constexpr (VALUE_TYPE == SensorValueType::U_QWORD) {
return registers_to_uint64(registers[0], registers[1], registers[2], registers[3]);
} else if constexpr (VALUE_TYPE == SensorValueType::U_QWORD_R) {
return registers_to_uint64(registers[3], registers[2], registers[1], registers[0]);
} else if constexpr (VALUE_TYPE == SensorValueType::S_QWORD) {
return static_cast<int64_t>(registers_to_uint64(registers[0], registers[1], registers[2], registers[3]));
} else if constexpr (VALUE_TYPE == SensorValueType::S_QWORD_R) {
return static_cast<int64_t>(registers_to_uint64(registers[3], registers[2], registers[1], registers[0]));
} else {
static_assert(VALUE_TYPE_SUPPORTED<VALUE_TYPE>, "registers_to_value() does not support this value type");
}
}
/// The type registers_to_value() yields for a given value type. Distinct from modbus::RegisterValues,
/// which is a container of raw words.
template<SensorValueType VALUE_TYPE>
using RegisterValueType = decltype(registers_to_value<VALUE_TYPE>(static_cast<const uint16_t *>(nullptr)));
/** The value stored at an absolute register address, or nullopt when it is not wholly inside this
* response. Lets a device decode by address rather than by offset, so a poll split across several
* requests needs no extra bookkeeping: a value outside the response simply yields nullopt.
* @param registers the response registers, in host byte order
* @param start_address the address the response begins at
* @param address the address of the wanted value
*/
template<SensorValueType VALUE_TYPE>
constexpr std::optional<RegisterValueType<VALUE_TYPE>> value_at(std::span<const uint16_t> registers,
uint16_t start_address, uint16_t address) {
if (address < start_address)
return std::nullopt;
const size_t offset = static_cast<size_t>(address) - start_address;
if (offset + register_width_for(VALUE_TYPE) > registers.size())
return std::nullopt;
return registers_to_value<VALUE_TYPE>(registers.data() + offset);
}
/// The widest standard numeric value (a QWORD) spans 4 registers, so one entity value never writes more.
static constexpr uint16_t MAX_FEW_REGISTERS = 4;
@@ -269,7 +269,8 @@ void ModbusServer::dump_config() {
" Enabled: %s\n"
" Register Last Address: 0x%02X\n"
" Register Value: %" PRIu16,
this->address_, this->server_courtesy_response_.enabled ? "true" : "false",
this->address_,
this->server_courtesy_response_.enabled ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
@@ -280,8 +281,9 @@ void ModbusServer::dump_config() {
}
ESP_LOGCONFIG(TAG, "server bits");
for (auto &b : this->server_bits_) {
ESP_LOGCONFIG(TAG, " Address=0x%04X readable=%s writable=%s", b->address, b->read_lambda ? "true" : "false",
b->write_lambda ? "true" : "false");
ESP_LOGCONFIG(TAG, " Address=0x%04X readable=%s writable=%s", b->address,
b->read_lambda ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
b->write_lambda ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
}
#endif
}
+3 -2
View File
@@ -595,7 +595,8 @@ void Nextion::process_nextion_commands_() {
uint8_t page_id = to_process[0];
uint8_t component_id = to_process[1];
uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press
ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id);
ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? LOG_STR_LITERAL("PRESS") : LOG_STR_LITERAL("RELEASE"),
page_id, component_id);
for (auto *touch : this->touch_) {
touch->process_touch(page_id, component_id, touch_event != 0);
}
@@ -628,7 +629,7 @@ void Nextion::process_nextion_commands_() {
const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1];
const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3];
const uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press
ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y);
ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? LOG_STR_LITERAL("PRESS") : LOG_STR_LITERAL("RELEASE"), x, y);
break;
}
+2 -1
View File
@@ -119,7 +119,8 @@ void PCM5122::dump_config() {
" Channel mix: %s\n"
" Volume range: %.1f dB to %.1f dB\n"
" Muted: %s",
this->bits_per_sample_, this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? "0 dB" : "-6 dB",
this->bits_per_sample_,
this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? LOG_STR_LITERAL("0 dB") : LOG_STR_LITERAL("-6 dB"),
channel_mix_str, this->volume_min_db_, this->volume_max_db_, YESNO(this->is_muted_));
LOG_PIN(" Enable Pin: ", this->enable_pin_);
}
+2 -2
View File
@@ -243,8 +243,8 @@ uint8_t PN7150::reset_core_(const bool reset_config, const bool power) {
}
ESP_LOGD(TAG, "Configuration %s, NCI version: %s",
rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained",
rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0");
rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? LOG_STR_LITERAL("reset") : LOG_STR_LITERAL("retained"),
rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? LOG_STR_LITERAL("2.0") : LOG_STR_LITERAL("1.0"));
return nfc::STATUS_OK;
}
+2 -2
View File
@@ -265,8 +265,8 @@ uint8_t PN7160::reset_core_(const bool reset_config, const bool power) {
}
ESP_LOGD(TAG, "Configuration %s, NCI version: %s, Manufacturer ID: 0x%02X",
rx.get_message()[4] ? "reset" : "retained", rx.get_message()[5] == 0x20 ? "2.0" : "1.0",
rx.get_message()[6]);
rx.get_message()[4] ? LOG_STR_LITERAL("reset") : LOG_STR_LITERAL("retained"),
rx.get_message()[5] == 0x20 ? LOG_STR_LITERAL("2.0") : LOG_STR_LITERAL("1.0"), rx.get_message()[6]);
rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8);
char mfr_buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
ESP_LOGD(TAG, "Manufacturer info: %s", nfc::format_bytes_to(mfr_buf, rx.get_message()));
+2 -1
View File
@@ -137,7 +137,8 @@ void PylontechComponent::process_line_(std::string &buffer) {
} else if (strcmp(token_buf, "Power") == 0) {
// header line i.e. "Power Volt Curr" and so on
this->has_tlow_id_ = buffer.find("Tlow.Id") != std::string::npos;
ESP_LOGD(TAG, "header line %s Tlow.Id: %s", this->has_tlow_id_ ? "with" : "without",
ESP_LOGD(TAG, "header line %s Tlow.Id: %s",
this->has_tlow_id_ ? LOG_STR_LITERAL("with") : LOG_STR_LITERAL("without"),
buffer.substr(0, buffer.size() - 2).c_str());
return;
} else {
+51 -49
View File
@@ -3,62 +3,64 @@
namespace esphome::pzemac {
namespace helpers = modbus::helpers;
static const char *const TAG = "pzemac";
static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42;
static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers
void PZEMAC::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
auto data = modbus::helpers::server_pdu_payload(response_pdu);
if (data.size() < 20) {
ESP_LOGW(TAG, "Invalid size for PZEM AC!");
// Register map, see https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809
// 32-bit values are two registers, low word first.
static const uint16_t PZEM_REGISTER_VOLTAGE = 0; // 1 register, 0.1 V
static const uint16_t PZEM_REGISTER_CURRENT = 1; // 2 registers, 0.001 A
static const uint16_t PZEM_REGISTER_ACTIVE_POWER = 3; // 2 registers, 0.1 W
static const uint16_t PZEM_REGISTER_ACTIVE_ENERGY = 5; // 2 registers, 1 Wh
static const uint16_t PZEM_REGISTER_FREQUENCY = 7; // 1 register, 0.1 Hz
static const uint16_t PZEM_REGISTER_POWER_FACTOR = 8; // 1 register, 0.01
void PZEMAC::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) {
if (!modbus::succeeded(status))
return; // the hub already logs exception responses
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
// correct for any read range, so the poll may be split into multiple requests.
auto publish_1_register = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
sensor->publish_state(*value / divisor);
};
auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD_R>(registers, start_address, reg))
sensor->publish_state(*value / divisor);
};
publish_1_register(this->voltage_sensor_, PZEM_REGISTER_VOLTAGE, 10.0f);
publish_2_registers(this->current_sensor_, PZEM_REGISTER_CURRENT, 1000.0f);
publish_2_registers(this->power_sensor_, PZEM_REGISTER_ACTIVE_POWER, 10.0f);
publish_2_registers(this->energy_sensor_, PZEM_REGISTER_ACTIVE_ENERGY, 1.0f);
publish_1_register(this->frequency_sensor_, PZEM_REGISTER_FREQUENCY, 10.0f);
publish_1_register(this->power_factor_sensor_, PZEM_REGISTER_POWER_FACTOR, 100.0f);
}
void PZEMAC::on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
modbus::ResponseStatus status) {
// The only custom request this component sends is the energy reset; acknowledge its echo here so
// the default unhandled-response warning stays meaningful.
if (!request_pdu.empty() && request_pdu[0] == PZEM_CMD_RESET_ENERGY) {
if (modbus::succeeded(status)) {
ESP_LOGD(TAG, "Energy reset acknowledged");
} else {
ESP_LOGW(TAG, "Energy reset rejected");
}
return;
}
// See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34
// Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc--
// 0 2 6 10 14 16
auto pzem_get_16bit = [&](size_t i) -> uint16_t {
return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0);
};
auto pzem_get_32bit = [&](size_t i) -> uint32_t {
return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0);
};
uint16_t raw_voltage = pzem_get_16bit(0);
float voltage = raw_voltage / 10.0f; // max 6553.5 V
uint32_t raw_current = pzem_get_32bit(2);
float current = raw_current / 1000.0f; // max 4294967.295 A
uint32_t raw_active_power = pzem_get_32bit(6);
float active_power = raw_active_power / 10.0f; // max 429496729.5 W
float active_energy = static_cast<float>(pzem_get_32bit(10));
uint16_t raw_frequency = pzem_get_16bit(14);
float frequency = raw_frequency / 10.0f;
uint16_t raw_power_factor = pzem_get_16bit(16);
float power_factor = raw_power_factor / 100.0f;
ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, E=%.1f Wh, F=%.1f Hz, PF=%.2f", voltage, current, active_power,
active_energy, frequency, power_factor);
if (this->voltage_sensor_ != nullptr)
this->voltage_sensor_->publish_state(voltage);
if (this->current_sensor_ != nullptr)
this->current_sensor_->publish_state(current);
if (this->power_sensor_ != nullptr)
this->power_sensor_->publish_state(active_power);
if (this->energy_sensor_ != nullptr)
this->energy_sensor_->publish_state(active_energy);
if (this->frequency_sensor_ != nullptr)
this->frequency_sensor_->publish_state(frequency);
if (this->power_factor_sensor_ != nullptr)
this->power_factor_sensor_->publish_state(power_factor);
modbus::ModbusClientDevice::on_custom_response(request_pdu, response_pdu, status);
}
void PZEMAC::update() { this->read_input_registers(0, PZEM_REGISTER_COUNT); }
+4 -1
View File
@@ -22,7 +22,10 @@ class PZEMAC final : public PollingComponent, public modbus::ModbusClientDevice
void update() override;
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) override;
void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
modbus::ResponseStatus status) override;
void dump_config() override;
+46 -38
View File
@@ -3,55 +3,63 @@
namespace esphome::pzemdc {
namespace helpers = modbus::helpers;
static const char *const TAG = "pzemdc";
static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42;
static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers
static const uint8_t PZEM_REGISTER_COUNT = 8; // 8x 16-bit registers
void PZEMDC::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
auto data = modbus::helpers::server_pdu_payload(response_pdu);
if (data.size() < 16) {
ESP_LOGW(TAG, "Invalid size for PZEM DC!");
return;
}
// Register map, see https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809
// 32-bit values are two registers, low word first.
static const uint16_t PZEM_REGISTER_VOLTAGE = 0; // 1 register, 0.01 V
static const uint16_t PZEM_REGISTER_CURRENT = 1; // 1 register, 0.01 A
static const uint16_t PZEM_REGISTER_POWER = 2; // 2 registers, 0.1 W
static const uint16_t PZEM_REGISTER_ENERGY = 4; // 2 registers, 1 Wh
// See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809
// 0 1 2 3 4 5 6 7 = ModBus register
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 = Buffer index
// 01 04 10 05 40 00 0A 00 0D 00 00 00 02 00 00 00 00 00 00 D6 29
// Id Cc Sz Volt- Curre Power------ Energy----- HiAlm LoAlm Crc--
void PZEMDC::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) {
if (!modbus::succeeded(status))
return; // the hub already logs exception responses
auto pzem_get_16bit = [&](size_t i) -> uint16_t {
return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0);
};
auto pzem_get_32bit = [&](size_t i) -> uint32_t {
return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0);
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
// correct for any read range, so the poll may be split into multiple requests.
auto publish_1_register = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
sensor->publish_state(*value / divisor);
};
uint16_t raw_voltage = pzem_get_16bit(0);
float voltage = raw_voltage / 100.0f; // max 655.35 V
auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD_R>(registers, start_address, reg))
sensor->publish_state(*value / divisor);
};
uint16_t raw_current = pzem_get_16bit(2);
float current = raw_current / 100.0f; // max 655.35 A
uint32_t raw_power = pzem_get_32bit(4);
float power = raw_power / 10.0f; // max 429496729.5 W
uint32_t raw_energy = pzem_get_32bit(8);
float energy = raw_energy / 1000.0f; // max 4294967.295 kWh
ESP_LOGD(TAG, "PZEM DC: V=%.1f V, I=%.3f A, P=%.1f W", voltage, current, power);
if (this->voltage_sensor_ != nullptr)
this->voltage_sensor_->publish_state(voltage);
if (this->current_sensor_ != nullptr)
this->current_sensor_->publish_state(current);
if (this->power_sensor_ != nullptr)
this->power_sensor_->publish_state(power);
if (this->energy_sensor_ != nullptr)
this->energy_sensor_->publish_state(energy);
publish_1_register(this->voltage_sensor_, PZEM_REGISTER_VOLTAGE, 100.0f);
publish_1_register(this->current_sensor_, PZEM_REGISTER_CURRENT, 100.0f);
publish_2_registers(this->power_sensor_, PZEM_REGISTER_POWER, 10.0f);
publish_2_registers(this->energy_sensor_, PZEM_REGISTER_ENERGY, 1000.0f);
}
void PZEMDC::update() { this->read_input_registers(0, 8); }
void PZEMDC::on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
modbus::ResponseStatus status) {
// The only custom request this component sends is the energy reset; acknowledge its echo here so
// the default unhandled-response warning stays meaningful.
if (!request_pdu.empty() && request_pdu[0] == PZEM_CMD_RESET_ENERGY) {
if (modbus::succeeded(status)) {
ESP_LOGD(TAG, "Energy reset acknowledged");
} else {
ESP_LOGW(TAG, "Energy reset rejected");
}
return;
}
modbus::ModbusClientDevice::on_custom_response(request_pdu, response_pdu, status);
}
void PZEMDC::update() { this->read_input_registers(0, PZEM_REGISTER_COUNT); }
void PZEMDC::dump_config() {
ESP_LOGCONFIG(TAG,
"PZEMDC:\n"
+4 -1
View File
@@ -18,7 +18,10 @@ class PZEMDC final : public PollingComponent, public modbus::ModbusClientDevice
void update() override;
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) override;
void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
modbus::ResponseStatus status) override;
void dump_config() override;
+3 -2
View File
@@ -55,8 +55,9 @@ void RD03DComponent::setup() {
void RD03DComponent::dump_config() {
ESP_LOGCONFIG(TAG, "RD-03D:");
if (this->tracking_mode_.has_value()) {
ESP_LOGCONFIG(TAG, " Tracking Mode: %s",
*this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? "single" : "multi");
ESP_LOGCONFIG(
TAG, " Tracking Mode: %s",
*this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? LOG_STR_LITERAL("single") : LOG_STR_LITERAL("multi"));
}
if (this->throttle_ > 0) {
ESP_LOGCONFIG(TAG, " Throttle: %" PRIu32 "ms", this->throttle_);
@@ -76,15 +76,16 @@ void RemoteReceiverComponent::setup() {
}
void RemoteReceiverComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Remote Receiver:\n"
" Buffer Size: %" PRIu32 "\n"
" Tolerance: %" PRIu32 "%s\n"
" Filter out pulses shorter than: %" PRIu32 " us\n"
" Signal is done after %" PRIu32 " us of no changes",
this->buffer_size_, this->tolerance_,
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_,
this->idle_us_);
ESP_LOGCONFIG(
TAG,
"Remote Receiver:\n"
" Buffer Size: %" PRIu32 "\n"
" Tolerance: %" PRIu32 "%s\n"
" Filter out pulses shorter than: %" PRIu32 " us\n"
" Signal is done after %" PRIu32 " us of no changes",
this->buffer_size_, this->tolerance_,
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"),
this->filter_us_, this->idle_us_);
LOG_PIN(" Pin: ", this->pin_);
}
@@ -11,8 +11,8 @@ void ResistanceSensor::dump_config() {
" Configuration: %s\n"
" Resistor: %.2fΩ\n"
" Reference Voltage: %.1fV",
this->configuration_ == UPSTREAM ? "UPSTREAM" : "DOWNSTREAM", this->resistor_,
this->reference_voltage_);
this->configuration_ == UPSTREAM ? LOG_STR_LITERAL("UPSTREAM") : LOG_STR_LITERAL("DOWNSTREAM"),
this->resistor_, this->reference_voltage_);
}
void ResistanceSensor::process_(float value) {
if (std::isnan(value)) {
+28 -65
View File
@@ -1,85 +1,48 @@
#include "sdm_meter.h"
#include "sdm_meter_registers.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::sdm_meter {
namespace helpers = modbus::helpers;
static const char *const TAG = "sdm_meter";
static const uint8_t MODBUS_REGISTER_COUNT = 80; // 74 x 16-bit registers
static const uint8_t MODBUS_REGISTER_COUNT = 80; // 80 x 16-bit registers (40 float values)
void SDMMeter::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
auto data = modbus::helpers::server_pdu_payload(response_pdu);
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
ESP_LOGW(TAG, "Invalid size for SDMMeter!");
return;
}
void SDMMeter::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) {
if (!modbus::succeeded(status))
return; // the hub already logs exception responses
auto sdm_meter_get_float = [&](size_t i) -> float {
uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]);
float f;
memcpy(&f, &temp, sizeof(f));
return f;
// Publish a sensor if both of its registers are in this response; skipping absent registers keeps
// this correct for any read range, so the poll may be split into multiple requests.
auto publish = [&](uint16_t reg, sensor::Sensor *sensor) {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::FP32>(registers, start_address, reg))
sensor->publish_state(*value);
};
for (uint8_t i = 0; i < 3; i++) {
auto phase = this->phases_[i];
auto &phase = this->phases_[i];
if (!phase.setup)
continue;
float voltage = sdm_meter_get_float(SDM_PHASE_1_VOLTAGE * 2 + (i * 4));
float current = sdm_meter_get_float(SDM_PHASE_1_CURRENT * 2 + (i * 4));
float active_power = sdm_meter_get_float(SDM_PHASE_1_ACTIVE_POWER * 2 + (i * 4));
float apparent_power = sdm_meter_get_float(SDM_PHASE_1_APPARENT_POWER * 2 + (i * 4));
float reactive_power = sdm_meter_get_float(SDM_PHASE_1_REACTIVE_POWER * 2 + (i * 4));
float power_factor = sdm_meter_get_float(SDM_PHASE_1_POWER_FACTOR * 2 + (i * 4));
float phase_angle = sdm_meter_get_float(SDM_PHASE_1_ANGLE * 2 + (i * 4));
ESP_LOGD(
TAG,
"SDMMeter Phase %c: V=%.3f V, I=%.3f A, Active P=%.3f W, Apparent P=%.3f VA, Reactive P=%.3f var, PF=%.3f, "
"PA=%.3f °",
i + 'A', voltage, current, active_power, apparent_power, reactive_power, power_factor, phase_angle);
if (phase.voltage_sensor_ != nullptr)
phase.voltage_sensor_->publish_state(voltage);
if (phase.current_sensor_ != nullptr)
phase.current_sensor_->publish_state(current);
if (phase.active_power_sensor_ != nullptr)
phase.active_power_sensor_->publish_state(active_power);
if (phase.apparent_power_sensor_ != nullptr)
phase.apparent_power_sensor_->publish_state(apparent_power);
if (phase.reactive_power_sensor_ != nullptr)
phase.reactive_power_sensor_->publish_state(reactive_power);
if (phase.power_factor_sensor_ != nullptr)
phase.power_factor_sensor_->publish_state(power_factor);
if (phase.phase_angle_sensor_ != nullptr)
phase.phase_angle_sensor_->publish_state(phase_angle);
publish(SDM_PHASE_1_VOLTAGE + i * 2, phase.voltage_sensor_);
publish(SDM_PHASE_1_CURRENT + i * 2, phase.current_sensor_);
publish(SDM_PHASE_1_ACTIVE_POWER + i * 2, phase.active_power_sensor_);
publish(SDM_PHASE_1_APPARENT_POWER + i * 2, phase.apparent_power_sensor_);
publish(SDM_PHASE_1_REACTIVE_POWER + i * 2, phase.reactive_power_sensor_);
publish(SDM_PHASE_1_POWER_FACTOR + i * 2, phase.power_factor_sensor_);
publish(SDM_PHASE_1_ANGLE + i * 2, phase.phase_angle_sensor_);
}
float total_power = sdm_meter_get_float(SDM_TOTAL_SYSTEM_POWER * 2);
float frequency = sdm_meter_get_float(SDM_FREQUENCY * 2);
float import_active_energy = sdm_meter_get_float(SDM_IMPORT_ACTIVE_ENERGY * 2);
float export_active_energy = sdm_meter_get_float(SDM_EXPORT_ACTIVE_ENERGY * 2);
float import_reactive_energy = sdm_meter_get_float(SDM_IMPORT_REACTIVE_ENERGY * 2);
float export_reactive_energy = sdm_meter_get_float(SDM_EXPORT_REACTIVE_ENERGY * 2);
ESP_LOGD(TAG, "SDMMeter: F=%.3f Hz, Im.A.E=%.3f Wh, Ex.A.E=%.3f Wh, Im.R.E=%.3f VARh, Ex.R.E=%.3f VARh, T.P=%.3f W",
frequency, import_active_energy, export_active_energy, import_reactive_energy, export_reactive_energy,
total_power);
if (this->total_power_sensor_ != nullptr)
this->total_power_sensor_->publish_state(total_power);
if (this->frequency_sensor_ != nullptr)
this->frequency_sensor_->publish_state(frequency);
if (this->import_active_energy_sensor_ != nullptr)
this->import_active_energy_sensor_->publish_state(import_active_energy);
if (this->export_active_energy_sensor_ != nullptr)
this->export_active_energy_sensor_->publish_state(export_active_energy);
if (this->import_reactive_energy_sensor_ != nullptr)
this->import_reactive_energy_sensor_->publish_state(import_reactive_energy);
if (this->export_reactive_energy_sensor_ != nullptr)
this->export_reactive_energy_sensor_->publish_state(export_reactive_energy);
publish(SDM_TOTAL_SYSTEM_POWER, this->total_power_sensor_);
publish(SDM_FREQUENCY, this->frequency_sensor_);
publish(SDM_IMPORT_ACTIVE_ENERGY, this->import_active_energy_sensor_);
publish(SDM_EXPORT_ACTIVE_ENERGY, this->export_active_energy_sensor_);
publish(SDM_IMPORT_REACTIVE_ENERGY, this->import_reactive_energy_sensor_);
publish(SDM_EXPORT_REACTIVE_ENERGY, this->export_reactive_energy_sensor_);
}
void SDMMeter::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT); }
+2 -1
View File
@@ -55,7 +55,8 @@ class SDMMeter final : public PollingComponent, public modbus::ModbusClientDevic
void update() override;
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) override;
void dump_config() override;
+31 -68
View File
@@ -1,84 +1,47 @@
#include "selec_meter.h"
#include "selec_meter_registers.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::selec_meter {
namespace helpers = modbus::helpers;
static const char *const TAG = "selec_meter";
static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers
void SelecMeter::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
auto data = modbus::helpers::server_pdu_payload(response_pdu);
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
ESP_LOGW(TAG, "Invalid size for SelecMeter!");
return;
}
void SelecMeter::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) {
if (!modbus::succeeded(status))
return; // the hub already logs exception responses
auto selec_meter_get_float = [&](size_t i, float unit) -> float {
uint32_t temp = encode_uint32(data[i + 2], data[i + 3], data[i], data[i + 1]);
float f;
memcpy(&f, &temp, sizeof(f));
return (f * unit);
// Publish a sensor if both of its registers are in this response; skipping absent registers keeps
// this correct for any read range, so the poll may be split into multiple requests.
// Values are 32-bit floats, low word first.
auto publish = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::FP32_R>(registers, start_address, reg))
sensor->publish_state(*value * unit);
};
float total_active_energy = selec_meter_get_float(SELEC_TOTAL_ACTIVE_ENERGY * 2, NO_DEC_UNIT);
float import_active_energy = selec_meter_get_float(SELEC_IMPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT);
float export_active_energy = selec_meter_get_float(SELEC_EXPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT);
float total_reactive_energy = selec_meter_get_float(SELEC_TOTAL_REACTIVE_ENERGY * 2, NO_DEC_UNIT);
float import_reactive_energy = selec_meter_get_float(SELEC_IMPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT);
float export_reactive_energy = selec_meter_get_float(SELEC_EXPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT);
float apparent_energy = selec_meter_get_float(SELEC_APPARENT_ENERGY * 2, NO_DEC_UNIT);
float active_power = selec_meter_get_float(SELEC_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT);
float reactive_power = selec_meter_get_float(SELEC_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT);
float apparent_power = selec_meter_get_float(SELEC_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT);
float voltage = selec_meter_get_float(SELEC_VOLTAGE * 2, NO_DEC_UNIT);
float current = selec_meter_get_float(SELEC_CURRENT * 2, NO_DEC_UNIT);
float power_factor = selec_meter_get_float(SELEC_POWER_FACTOR * 2, NO_DEC_UNIT);
float frequency = selec_meter_get_float(SELEC_FREQUENCY * 2, NO_DEC_UNIT);
float maximum_demand_active_power =
selec_meter_get_float(SELEC_MAXIMUM_DEMAND_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT);
float maximum_demand_reactive_power =
selec_meter_get_float(SELEC_MAXIMUM_DEMAND_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT);
float maximum_demand_apparent_power =
selec_meter_get_float(SELEC_MAXIMUM_DEMAND_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT);
if (this->total_active_energy_sensor_ != nullptr)
this->total_active_energy_sensor_->publish_state(total_active_energy);
if (this->import_active_energy_sensor_ != nullptr)
this->import_active_energy_sensor_->publish_state(import_active_energy);
if (this->export_active_energy_sensor_ != nullptr)
this->export_active_energy_sensor_->publish_state(export_active_energy);
if (this->total_reactive_energy_sensor_ != nullptr)
this->total_reactive_energy_sensor_->publish_state(total_reactive_energy);
if (this->import_reactive_energy_sensor_ != nullptr)
this->import_reactive_energy_sensor_->publish_state(import_reactive_energy);
if (this->export_reactive_energy_sensor_ != nullptr)
this->export_reactive_energy_sensor_->publish_state(export_reactive_energy);
if (this->apparent_energy_sensor_ != nullptr)
this->apparent_energy_sensor_->publish_state(apparent_energy);
if (this->active_power_sensor_ != nullptr)
this->active_power_sensor_->publish_state(active_power);
if (this->reactive_power_sensor_ != nullptr)
this->reactive_power_sensor_->publish_state(reactive_power);
if (this->apparent_power_sensor_ != nullptr)
this->apparent_power_sensor_->publish_state(apparent_power);
if (this->voltage_sensor_ != nullptr)
this->voltage_sensor_->publish_state(voltage);
if (this->current_sensor_ != nullptr)
this->current_sensor_->publish_state(current);
if (this->power_factor_sensor_ != nullptr)
this->power_factor_sensor_->publish_state(power_factor);
if (this->frequency_sensor_ != nullptr)
this->frequency_sensor_->publish_state(frequency);
if (this->maximum_demand_active_power_sensor_ != nullptr)
this->maximum_demand_active_power_sensor_->publish_state(maximum_demand_active_power);
if (this->maximum_demand_reactive_power_sensor_ != nullptr)
this->maximum_demand_reactive_power_sensor_->publish_state(maximum_demand_reactive_power);
if (this->maximum_demand_apparent_power_sensor_ != nullptr)
this->maximum_demand_apparent_power_sensor_->publish_state(maximum_demand_apparent_power);
publish(this->total_active_energy_sensor_, SELEC_TOTAL_ACTIVE_ENERGY, NO_DEC_UNIT);
publish(this->import_active_energy_sensor_, SELEC_IMPORT_ACTIVE_ENERGY, NO_DEC_UNIT);
publish(this->export_active_energy_sensor_, SELEC_EXPORT_ACTIVE_ENERGY, NO_DEC_UNIT);
publish(this->total_reactive_energy_sensor_, SELEC_TOTAL_REACTIVE_ENERGY, NO_DEC_UNIT);
publish(this->import_reactive_energy_sensor_, SELEC_IMPORT_REACTIVE_ENERGY, NO_DEC_UNIT);
publish(this->export_reactive_energy_sensor_, SELEC_EXPORT_REACTIVE_ENERGY, NO_DEC_UNIT);
publish(this->apparent_energy_sensor_, SELEC_APPARENT_ENERGY, NO_DEC_UNIT);
publish(this->active_power_sensor_, SELEC_ACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
publish(this->reactive_power_sensor_, SELEC_REACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
publish(this->apparent_power_sensor_, SELEC_APPARENT_POWER, MULTIPLY_THOUSAND_UNIT);
publish(this->voltage_sensor_, SELEC_VOLTAGE, NO_DEC_UNIT);
publish(this->current_sensor_, SELEC_CURRENT, NO_DEC_UNIT);
publish(this->power_factor_sensor_, SELEC_POWER_FACTOR, NO_DEC_UNIT);
publish(this->frequency_sensor_, SELEC_FREQUENCY, NO_DEC_UNIT);
publish(this->maximum_demand_active_power_sensor_, SELEC_MAXIMUM_DEMAND_ACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
publish(this->maximum_demand_reactive_power_sensor_, SELEC_MAXIMUM_DEMAND_REACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
publish(this->maximum_demand_apparent_power_sensor_, SELEC_MAXIMUM_DEMAND_APPARENT_POWER, MULTIPLY_THOUSAND_UNIT);
}
void SelecMeter::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT); }
+2 -1
View File
@@ -37,7 +37,8 @@ class SelecMeter final : public PollingComponent, public modbus::ModbusClientDev
void update() override;
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) override;
void dump_config() override;
};
+1 -1
View File
@@ -13,7 +13,7 @@ void Sen21231Sensor::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
ESP_LOGI(TAG, "SEN21231: %s", this->is_failed() ? "FAILED" : "OK");
ESP_LOGI(TAG, "SEN21231: %s", this->is_failed() ? LOG_STR_LITERAL("FAILED") : LOG_STR_LITERAL("OK"));
LOG_UPDATE_INTERVAL(this);
}
+69 -10
View File
@@ -9,9 +9,11 @@ static const char *const TAG = "sen6x";
static constexpr uint8_t POLL_RETRIES = 24; // 24 attempts
static constexpr uint32_t I2C_READ_DELAY = 20; // 20 ms to wait for I2C read to complete
static constexpr uint32_t CMD_EXEC_DELAY = 20; // execution time of set commands (datasheet section 4.8)
static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts
// Single numeric timeout ID — the chain is sequential so only one is active at a time.
// 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 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;
@@ -26,6 +28,8 @@ 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_RESET = 0xD304;
static constexpr uint16_t SEN6X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
static constexpr uint16_t SEN6X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
static inline void set_read_command_and_words(SEN6XComponent::Sen6xType type, uint16_t &read_cmd, uint8_t &read_words) {
read_cmd = SEN6X_CMD_READ_MEASUREMENT;
@@ -143,21 +147,76 @@ void SEN6XComponent::setup() {
this->firmware_version_minor_ = raw_firmware_version & 0xFF;
ESP_LOGI(TAG, "Firmware: %u.%u", this->firmware_version_major_, this->firmware_version_minor_);
if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->set_timeout(60000, [this]() { this->startup_complete_ = true; });
this->initialized_ = true;
ESP_LOGD(TAG, "Initialized");
// Step 4: write configuration commands one at a time, then start measurements.
// Delay the first step so it doesn't run in the same loop tick as the read above.
this->set_timeout(TIMEOUT_SETUP_STEP, CMD_EXEC_DELAY, [this]() { this->run_next_setup_step_(); });
});
});
});
});
}
// One configuration write per invocation, spaced by CMD_EXEC_DELAY. Cases without a
// configured value fall through; each taken case must advance setup_step_index_ so the
// next invocation resumes at the following step. These writes are optional, so a failure
// only warns and the chain continues to the mandatory start-measurements write.
void SEN6XComponent::run_next_setup_step_() {
switch (this->setup_step_index_) {
// Tuning writes are skipped when setup() disabled the sensor for this variant
case 0:
this->setup_step_index_++;
if (this->voc_sensor_ != nullptr && this->voc_tuning_params_.has_value()) {
this->write_tuning_parameters_(SEN6X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
break;
}
[[fallthrough]];
case 1:
this->setup_step_index_++;
if (this->nox_sensor_ != nullptr && this->nox_tuning_params_.has_value()) {
this->write_tuning_parameters_(SEN6X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
break;
}
[[fallthrough]];
default:
this->finish_setup_();
return;
}
this->set_timeout(TIMEOUT_SETUP_STEP, CMD_EXEC_DELAY, [this]() { this->run_next_setup_step_(); });
}
void SEN6XComponent::finish_setup_() {
if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
ESP_LOGE(TAG, "Write 0x%04X failed, error %d", SEN6X_CMD_START_MEASUREMENTS, this->last_error_);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->set_timeout(60000, [this]() { this->startup_complete_ = true; });
this->initialized_ = true;
ESP_LOGD(TAG, "Initialized");
}
// Writes one optional configuration command. A failure warns and returns false, but does
// not stop setup: the sensor still measures with that setting left at its default.
bool SEN6XComponent::write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len) {
if (!this->write_command(i2c_command, data, len)) {
ESP_LOGE(TAG, "Write 0x%04X failed, error %d", i2c_command, this->last_error_);
this->status_set_warning();
return false;
}
return true;
}
bool SEN6XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) {
uint16_t params[6] = {tuning.index_offset,
tuning.learning_time_offset_hours,
tuning.learning_time_gain_hours,
tuning.gating_max_duration_minutes,
tuning.std_initial,
tuning.gain_factor};
return this->write_config_words_(i2c_command, params, 6);
}
void SEN6XComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"sen6x:\n"
+40 -2
View File
@@ -1,11 +1,25 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/optional.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h"
namespace esphome::sen6x {
// The NOx algorithm requires std_initial to stay at 50 (Sensirion datasheet)
static constexpr uint16_t NOX_STD_INITIAL = 50;
// Raw parameter block for the VOC/NOx algorithm tuning commands
struct GasTuning {
uint16_t index_offset;
uint16_t learning_time_offset_hours;
uint16_t learning_time_gain_hours;
uint16_t gating_max_duration_minutes;
uint16_t std_initial;
uint16_t gain_factor;
};
class SEN6XComponent final : public PollingComponent, public sensirion_common::SensirionI2CDevice {
SUB_SENSOR(pm_1_0)
SUB_SENSOR(pm_2_5)
@@ -27,22 +41,46 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
enum Sen6xType { SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C, UNKNOWN };
void set_type(const std::string &type) { sen6x_type_ = infer_type_from_product_name_(type); }
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) {
this->voc_tuning_params_ = GasTuning{
index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial,
gain_factor};
}
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t gain_factor) {
this->nox_tuning_params_ = GasTuning{index_offset,
learning_time_offset_hours,
learning_time_gain_hours,
gating_max_duration_minutes,
NOX_STD_INITIAL,
gain_factor};
}
protected:
Sen6xType infer_type_from_product_name_(const std::string &product_name);
void run_next_setup_step_();
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);
void poll_data_ready_();
void read_measurements_();
void parse_and_publish_measurements_();
bool initialized_{false};
std::string product_name_;
Sen6xType sen6x_type_{UNKNOWN};
std::string serial_number_;
optional<GasTuning> voc_tuning_params_;
optional<GasTuning> nox_tuning_params_;
Sen6xType sen6x_type_{UNKNOWN};
uint16_t read_cmd_{0};
uint8_t setup_step_index_{0};
uint8_t firmware_version_major_{0};
uint8_t firmware_version_minor_{0};
uint8_t poll_retries_remaining_{0};
uint8_t read_words_{0};
bool initialized_{false};
bool startup_complete_{false};
};
+68 -8
View File
@@ -3,15 +3,22 @@ from esphome.components import i2c, sensirion_common, sensor
from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX
import esphome.config_validation as cv
from esphome.const import (
CONF_ALGORITHM_TUNING,
CONF_CO2,
CONF_FORMALDEHYDE,
CONF_GAIN_FACTOR,
CONF_GATING_MAX_DURATION_MINUTES,
CONF_HUMIDITY,
CONF_ID,
CONF_INDEX_OFFSET,
CONF_LEARNING_TIME_GAIN_HOURS,
CONF_LEARNING_TIME_OFFSET_HOURS,
CONF_NOX,
CONF_PM_1_0,
CONF_PM_2_5,
CONF_PM_4_0,
CONF_PM_10_0,
CONF_STD_INITIAL,
CONF_TEMPERATURE,
CONF_TYPE,
CONF_VOC,
@@ -44,6 +51,42 @@ SEN6XComponent = sen6x_ns.class_(
)
def _gas_index_schema(
*,
index_offset: int,
gating_max_duration: int,
std_initial: int | None,
) -> cv.Schema:
"""Sensor schema for a gas index sensor with optional algorithm tuning.
std_initial is only configurable for VOC; the NOx algorithm requires 50.
"""
tuning_schema = {
cv.Optional(CONF_INDEX_OFFSET, default=index_offset): cv.int_range(
min=1, max=250
),
cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range(
min=1, max=1000
),
cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range(
min=1, max=1000
),
cv.Optional(
CONF_GATING_MAX_DURATION_MINUTES, default=gating_max_duration
): cv.int_range(min=0, max=3000),
cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(min=1, max=1000),
}
if std_initial is not None:
tuning_schema[cv.Optional(CONF_STD_INITIAL, default=std_initial)] = (
cv.int_range(min=10, max=5000)
)
return sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
).extend({cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(tuning_schema)})
CONFIG_SCHEMA = cv.All(
cv.rename_key(CONF_VOC, CONF_VOC_INDEX, removed_in="2027.2.0", component="sen6x"),
cv.rename_key(CONF_NOX, CONF_NOX_INDEX, removed_in="2027.2.0", component="sen6x"),
@@ -94,15 +137,15 @@ CONFIG_SCHEMA = cv.All(
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VOC_INDEX): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
cv.Optional(CONF_VOC_INDEX): _gas_index_schema(
index_offset=100,
gating_max_duration=180,
std_initial=50,
),
cv.Optional(CONF_NOX_INDEX): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
cv.Optional(CONF_NOX_INDEX): _gas_index_schema(
index_offset=1,
gating_max_duration=720,
std_initial=None,
),
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
@@ -149,3 +192,20 @@ async def to_code(config: ConfigType) -> None:
if cfg := config.get(key):
sens = await sensor.new_sensor(cfg)
cg.add(getattr(var, func_name)(sens))
for key, setter in (
(CONF_VOC_INDEX, "set_voc_algorithm_tuning"),
(CONF_NOX_INDEX, "set_nox_algorithm_tuning"),
):
if (tuning := config.get(key, {}).get(CONF_ALGORITHM_TUNING)) is not None:
args = [
tuning[CONF_INDEX_OFFSET],
tuning[CONF_LEARNING_TIME_OFFSET_HOURS],
tuning[CONF_LEARNING_TIME_GAIN_HOURS],
tuning[CONF_GATING_MAX_DURATION_MINUTES],
]
# std_initial is in the schema for VOC only
if (std_initial := tuning.get(CONF_STD_INITIAL)) is not None:
args.append(std_initial)
args.append(tuning[CONF_GAIN_FACTOR])
cg.add(getattr(var, setter)(*args))
+3 -2
View File
@@ -89,8 +89,9 @@ void SenseAirComponent::background_calibration_result() {
}
// Check if 5th bit (register CI6) is set
ESP_LOGI(TAG, "SenseAir Result=%s (%02x%02x%02x %02x%02x %02x%02x)", (response[4] & 0b100000) != 0 ? "OK" : "NOT_OK",
response[0], response[1], response[2], response[3], response[4], response[5], response[6]);
ESP_LOGI(TAG, "SenseAir Result=%s (%02x%02x%02x %02x%02x %02x%02x)",
(response[4] & 0b100000) != 0 ? LOG_STR_LITERAL("OK") : LOG_STR_LITERAL("NOT_OK"), response[0], response[1],
response[2], response[3], response[4], response[5], response[6]);
}
void SenseAirComponent::abc_enable() {
@@ -82,11 +82,11 @@ void SerialProxy::dump_config() {
" RTS Pin: %s\n"
" DTR Pin: %s",
this->instance_index_, this->name_ != nullptr ? this->name_ : "",
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? "RS485"
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? "RS232"
: "TTL",
this->rts_pin_ != nullptr ? "configured" : "not configured",
this->dtr_pin_ != nullptr ? "configured" : "not configured");
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
: LOG_STR_LITERAL("TTL"),
this->rts_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"),
this->dtr_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"));
}
SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control,

Some files were not shown because too many files have changed in this diff Show More