mirror of
https://github.com/esphome/esphome.git
synced 2025-11-19 08:15:49 +00:00
Compare commits
42 Commits
switch_to_
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c4ab546de | ||
|
|
8e00e895e0 | ||
|
|
9dba37962c | ||
|
|
11d0d4d128 | ||
|
|
a4242dee64 | ||
|
|
0d6c9623ce | ||
|
|
0923bcd2ca | ||
|
|
fdc7ae7760 | ||
|
|
1a73f49cd2 | ||
|
|
23f85162d0 | ||
|
|
7a238028a7 | ||
|
|
3d6c361037 | ||
|
|
9e1f8d83f8 | ||
|
|
fa0aa6defc | ||
|
|
70366d2124 | ||
|
|
10bdb47eae | ||
|
|
a38c4e0c6e | ||
|
|
6c6b03bda0 | ||
|
|
9e02e31917 | ||
|
|
3fd58f1a91 | ||
|
|
9151489481 | ||
|
|
f19296ac7f | ||
|
|
36868ee7b1 | ||
|
|
d559f9f52e | ||
|
|
6440b5fbf5 | ||
|
|
97c4914573 | ||
|
|
7ce94c27fe | ||
|
|
eb54c0026d | ||
|
|
fe00e209ff | ||
|
|
aed80732f9 | ||
|
|
aa097a2fe6 | ||
|
|
3b860e784c | ||
|
|
96ee38759d | ||
|
|
986d3c8f13 | ||
|
|
320120883c | ||
|
|
4fc4da6ed2 | ||
|
|
6f4042f401 | ||
|
|
ea2b4c3e25 | ||
|
|
fc546ca3f6 | ||
|
|
6b158e760d | ||
|
|
5710cab972 | ||
|
|
eb759efb3d |
2
.github/workflows/auto-label-pr.yml
vendored
2
.github/workflows/auto-label-pr.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
|
||||
2
.github/workflows/ci-api-proto.yml
vendored
2
.github/workflows/ci-api-proto.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/ci-clang-tidy-hash.yml
vendored
2
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
|
||||
2
.github/workflows/ci-docker.yml
vendored
2
.github/workflows/ci-docker.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
- "docker"
|
||||
# - "lint"
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Check out code from base repository
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
# Always check out from the base repository (esphome/esphome), never from forks
|
||||
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||
|
||||
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
# Fetch enough history to find the merge base
|
||||
fetch-depth: 2
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
@@ -273,7 +273,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -489,7 +489,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -577,7 +577,7 @@ jobs:
|
||||
version: 1.0
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -662,7 +662,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -688,7 +688,7 @@ jobs:
|
||||
skip: ${{ steps.check-script.outputs.skip }}
|
||||
steps:
|
||||
- name: Check out target branch
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
@@ -840,7 +840,7 @@ jobs:
|
||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
||||
steps:
|
||||
- name: Check out PR branch
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -908,7 +908,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Get tag
|
||||
id: tag
|
||||
# yamllint disable rule:line-length
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
os: "ubuntu-24.04-arm"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
- ghcr
|
||||
- dockerhub
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
|
||||
4
.github/workflows/sync-device-classes.yml
vendored
4
.github/workflows/sync-device-classes.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
if: github.repository == 'esphome/esphome'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Checkout Home Assistant
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
repository: home-assistant/core
|
||||
path: lib/home-assistant
|
||||
|
||||
@@ -15,6 +15,11 @@ from . import (
|
||||
class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
"""Memory analyzer with CLI-specific report generation."""
|
||||
|
||||
# Symbol size threshold for detailed analysis
|
||||
SYMBOL_SIZE_THRESHOLD: int = (
|
||||
100 # Show symbols larger than this in detailed analysis
|
||||
)
|
||||
|
||||
# Column width constants
|
||||
COL_COMPONENT: int = 29
|
||||
COL_FLASH_TEXT: int = 14
|
||||
@@ -191,14 +196,21 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%"
|
||||
)
|
||||
|
||||
# Top 15 largest core symbols
|
||||
# All core symbols above threshold
|
||||
lines.append("")
|
||||
lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:")
|
||||
sorted_core_symbols = sorted(
|
||||
self._esphome_core_symbols, key=lambda x: x[2], reverse=True
|
||||
)
|
||||
large_core_symbols = [
|
||||
(symbol, demangled, size)
|
||||
for symbol, demangled, size in sorted_core_symbols
|
||||
if size > self.SYMBOL_SIZE_THRESHOLD
|
||||
]
|
||||
|
||||
for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]):
|
||||
lines.append(
|
||||
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
|
||||
)
|
||||
for i, (symbol, demangled, size) in enumerate(large_core_symbols):
|
||||
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
|
||||
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
@@ -268,13 +280,15 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(f"Total size: {comp_mem.flash_total:,} B")
|
||||
lines.append("")
|
||||
|
||||
# Show all symbols > 100 bytes for better visibility
|
||||
# Show all symbols above threshold for better visibility
|
||||
large_symbols = [
|
||||
(sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
|
||||
(sym, dem, size)
|
||||
for sym, dem, size in sorted_symbols
|
||||
if size > self.SYMBOL_SIZE_THRESHOLD
|
||||
]
|
||||
|
||||
lines.append(
|
||||
f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):"
|
||||
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):"
|
||||
)
|
||||
for i, (symbol, demangled, size) in enumerate(large_symbols):
|
||||
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
|
||||
|
||||
@@ -51,14 +51,13 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
@@ -96,14 +95,13 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
|
||||
29
esphome/components/axp2101/__init__.py
Normal file
29
esphome/components/axp2101/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""AXP2101 Power Management IC component for ESPHome."""
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
|
||||
axp2101_ns = cg.esphome_ns.namespace("axp2101")
|
||||
AXP2101Component = axp2101_ns.class_("AXP2101Component", cg.PollingComponent, i2c.I2CDevice)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AXP2101Component),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x34))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate code for the AXP2101 component."""
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
369
esphome/components/axp2101/axp2101.cpp
Normal file
369
esphome/components/axp2101/axp2101.cpp
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* @file axp2101.cpp
|
||||
* @brief Implementation of AXP2101 Power Management IC Component
|
||||
*/
|
||||
#include "axp2101.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace axp2101 {
|
||||
|
||||
static const char *const TAG = "axp2101";
|
||||
|
||||
// Temperature conversion constant (from datasheet)
|
||||
static const float TEMP_CONVERSION_FACTOR = 0.1f;
|
||||
static const float TEMP_OFFSET = -267.15f;
|
||||
|
||||
void AXP2101Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up AXP2101...");
|
||||
|
||||
// Read chip ID to verify communication
|
||||
uint8_t chip_id;
|
||||
if (!this->read_byte(AXP2101_IC_TYPE, &chip_id)) {
|
||||
ESP_LOGE(TAG, "Failed to read chip ID");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Chip ID: 0x%02X", chip_id);
|
||||
|
||||
// Enable ADC channels for monitoring
|
||||
// Enable battery voltage, VBUS, VSYS, and temperature measurements
|
||||
uint8_t adc_ctrl = 0xFF; // Enable all ADC channels
|
||||
if (!this->write_byte(AXP2101_ADC_CHANNEL_CTRL, adc_ctrl)) {
|
||||
ESP_LOGW(TAG, "Failed to configure ADC channels");
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, "AXP2101 setup complete");
|
||||
}
|
||||
|
||||
void AXP2101Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AXP2101:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with AXP2101 failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// Log sensor configuration
|
||||
LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_sensor_);
|
||||
LOG_SENSOR(" ", "VBUS Voltage", this->vbus_voltage_sensor_);
|
||||
LOG_SENSOR(" ", "VSYS Voltage", this->vsys_voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Die Temperature", this->die_temperature_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AXP2101Component::update() {
|
||||
#ifdef USE_SENSOR
|
||||
if (this->battery_voltage_sensor_ != nullptr) {
|
||||
this->battery_voltage_sensor_->publish_state(this->read_battery_voltage() / 1000.0f);
|
||||
}
|
||||
|
||||
if (this->battery_level_sensor_ != nullptr) {
|
||||
this->battery_level_sensor_->publish_state(this->read_battery_level());
|
||||
}
|
||||
|
||||
if (this->vbus_voltage_sensor_ != nullptr) {
|
||||
this->vbus_voltage_sensor_->publish_state(this->read_vbus_voltage() / 1000.0f);
|
||||
}
|
||||
|
||||
if (this->vsys_voltage_sensor_ != nullptr) {
|
||||
this->vsys_voltage_sensor_->publish_state(this->read_vsys_voltage() / 1000.0f);
|
||||
}
|
||||
|
||||
if (this->die_temperature_sensor_ != nullptr) {
|
||||
this->die_temperature_sensor_->publish_state(this->read_die_temperature());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool AXP2101Component::set_register_bit(uint8_t reg, uint8_t bit) {
|
||||
uint8_t value;
|
||||
if (!this->read_byte(reg, &value)) {
|
||||
return false;
|
||||
}
|
||||
value |= (1 << bit);
|
||||
return this->write_byte(reg, value);
|
||||
}
|
||||
|
||||
bool AXP2101Component::clear_register_bit(uint8_t reg, uint8_t bit) {
|
||||
uint8_t value;
|
||||
if (!this->read_byte(reg, &value)) {
|
||||
return false;
|
||||
}
|
||||
value &= ~(1 << bit);
|
||||
return this->write_byte(reg, value);
|
||||
}
|
||||
|
||||
bool AXP2101Component::get_register_bit(uint8_t reg, uint8_t bit) {
|
||||
uint8_t value;
|
||||
if (!this->read_byte(reg, &value)) {
|
||||
return false;
|
||||
}
|
||||
return (value & (1 << bit)) != 0;
|
||||
}
|
||||
|
||||
uint16_t AXP2101Component::read_register_h5l8(uint8_t high_reg, uint8_t low_reg) {
|
||||
uint8_t high, low;
|
||||
if (!this->read_byte(high_reg, &high) || !this->read_byte(low_reg, &low)) {
|
||||
return 0;
|
||||
}
|
||||
return ((high & 0x1F) << 8) | low;
|
||||
}
|
||||
|
||||
uint16_t AXP2101Component::read_register_h6l8(uint8_t high_reg, uint8_t low_reg) {
|
||||
uint8_t high, low;
|
||||
if (!this->read_byte(high_reg, &high) || !this->read_byte(low_reg, &low)) {
|
||||
return 0;
|
||||
}
|
||||
return ((high & 0x3F) << 8) | low;
|
||||
}
|
||||
|
||||
bool AXP2101Component::get_rail_enable_info(PowerRail rail, uint8_t ®, uint8_t &bit) {
|
||||
// DCDC rails are controlled by DC_ONOFF_DVM_CTRL register
|
||||
if (rail >= DCDC1 && rail <= DCDC5) {
|
||||
reg = AXP2101_DC_ONOFF_DVM_CTRL;
|
||||
bit = static_cast<uint8_t>(rail); // DCDC1=bit0, DCDC2=bit1, etc.
|
||||
return true;
|
||||
}
|
||||
|
||||
// LDO rails are controlled by LDO_ONOFF_CTRL0 and LDO_ONOFF_CTRL1
|
||||
if (rail >= ALDO1 && rail <= DLDO2) {
|
||||
if (rail <= BLDO2) {
|
||||
reg = AXP2101_LDO_ONOFF_CTRL0;
|
||||
bit = static_cast<uint8_t>(rail - ALDO1); // ALDO1=bit0, ALDO2=bit1, etc.
|
||||
} else {
|
||||
reg = AXP2101_LDO_ONOFF_CTRL1;
|
||||
bit = static_cast<uint8_t>(rail - CPUSLDO); // CPUSLDO=bit0, DLDO1=bit1, DLDO2=bit2
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AXP2101Component::get_rail_voltage_info(PowerRail rail, uint8_t ®, uint16_t &min_mv, uint16_t &max_mv,
|
||||
uint16_t &step_mv) {
|
||||
switch (rail) {
|
||||
case DCDC1:
|
||||
reg = AXP2101_DC_VOL0_CTRL;
|
||||
min_mv = 1500;
|
||||
max_mv = 3400;
|
||||
step_mv = 100;
|
||||
return true;
|
||||
|
||||
case DCDC2:
|
||||
case DCDC3:
|
||||
case DCDC4:
|
||||
// These have dual ranges, handle in calculate methods
|
||||
reg = AXP2101_DC_VOL1_CTRL + (rail - DCDC2);
|
||||
min_mv = 500;
|
||||
max_mv = 1540;
|
||||
step_mv = 10;
|
||||
return true;
|
||||
|
||||
case DCDC5:
|
||||
reg = AXP2101_DC_VOL4_CTRL;
|
||||
min_mv = 1400;
|
||||
max_mv = 3700;
|
||||
step_mv = 100;
|
||||
return true;
|
||||
|
||||
case ALDO1:
|
||||
case ALDO2:
|
||||
case ALDO3:
|
||||
case ALDO4:
|
||||
reg = AXP2101_ALDO1_VOL_CTRL + (rail - ALDO1);
|
||||
min_mv = 500;
|
||||
max_mv = 3500;
|
||||
step_mv = 100;
|
||||
return true;
|
||||
|
||||
case BLDO1:
|
||||
case BLDO2:
|
||||
reg = AXP2101_BLDO1_VOL_CTRL + (rail - BLDO1);
|
||||
min_mv = 500;
|
||||
max_mv = 3500;
|
||||
step_mv = 100;
|
||||
return true;
|
||||
|
||||
case CPUSLDO:
|
||||
reg = AXP2101_CPUSLDO_VOL_CTRL;
|
||||
min_mv = 500;
|
||||
max_mv = 1400;
|
||||
step_mv = 50;
|
||||
return true;
|
||||
|
||||
case DLDO1:
|
||||
case DLDO2:
|
||||
reg = AXP2101_DLDO1_VOL_CTRL + (rail - DLDO1);
|
||||
min_mv = 500;
|
||||
max_mv = 3400;
|
||||
step_mv = 100;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t AXP2101Component::calculate_voltage_register_value(PowerRail rail, uint16_t millivolts) {
|
||||
// Special handling for DCDC2, DCDC3, DCDC4 with dual ranges
|
||||
if (rail == DCDC2 || rail == DCDC3 || rail == DCDC4) {
|
||||
if (millivolts >= 500 && millivolts <= 1200) {
|
||||
// Range 1: 500-1200mV in 10mV steps
|
||||
return static_cast<uint8_t>((millivolts - 500) / 10);
|
||||
} else if (millivolts >= 1220 && millivolts <= 1540) {
|
||||
// Range 2: 1220-1540mV in 20mV steps
|
||||
return static_cast<uint8_t>(70 + (millivolts - 1220) / 20);
|
||||
} else if (rail == DCDC3 && millivolts >= 1600 && millivolts <= 3400) {
|
||||
// DCDC3 has an extended range
|
||||
return static_cast<uint8_t>(87 + (millivolts - 1600) / 100);
|
||||
}
|
||||
return 0; // Invalid voltage
|
||||
}
|
||||
|
||||
// Special handling for DCDC4 extended range
|
||||
if (rail == DCDC4 && millivolts >= 1600 && millivolts <= 1840) {
|
||||
return static_cast<uint8_t>(87 + (millivolts - 1600) / 20);
|
||||
}
|
||||
|
||||
// Standard calculation for other rails
|
||||
uint8_t reg;
|
||||
uint16_t min_mv, max_mv, step_mv;
|
||||
if (!this->get_rail_voltage_info(rail, reg, min_mv, max_mv, step_mv)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (millivolts < min_mv || millivolts > max_mv) {
|
||||
return 0; // Out of range
|
||||
}
|
||||
|
||||
if ((millivolts - min_mv) % step_mv != 0) {
|
||||
return 0; // Not aligned to step size
|
||||
}
|
||||
|
||||
return static_cast<uint8_t>((millivolts - min_mv) / step_mv);
|
||||
}
|
||||
|
||||
uint16_t AXP2101Component::calculate_millivolts_from_register(PowerRail rail, uint8_t reg_value) {
|
||||
// Special handling for DCDC2, DCDC3, DCDC4 with dual ranges
|
||||
if (rail == DCDC2 || rail == DCDC3 || rail == DCDC4) {
|
||||
if (reg_value <= 70) {
|
||||
// Range 1: 500-1200mV in 10mV steps
|
||||
return 500 + (reg_value * 10);
|
||||
} else if (reg_value <= 86) {
|
||||
// Range 2: 1220-1540mV in 20mV steps
|
||||
return 1220 + ((reg_value - 70) * 20);
|
||||
} else if (rail == DCDC3 && reg_value <= 105) {
|
||||
// DCDC3 extended range: 1600-3400mV in 100mV steps
|
||||
return 1600 + ((reg_value - 87) * 100);
|
||||
} else if (rail == DCDC4 && reg_value <= 99) {
|
||||
// DCDC4 extended range: 1600-1840mV in 20mV steps
|
||||
return 1600 + ((reg_value - 87) * 20);
|
||||
}
|
||||
return 0; // Invalid
|
||||
}
|
||||
|
||||
// Standard calculation for other rails
|
||||
uint8_t reg;
|
||||
uint16_t min_mv, max_mv, step_mv;
|
||||
if (!this->get_rail_voltage_info(rail, reg, min_mv, max_mv, step_mv)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return min_mv + (reg_value * step_mv);
|
||||
}
|
||||
|
||||
bool AXP2101Component::enable_power_rail(PowerRail rail) {
|
||||
uint8_t reg, bit;
|
||||
if (!this->get_rail_enable_info(rail, reg, bit)) {
|
||||
return false;
|
||||
}
|
||||
return this->set_register_bit(reg, bit);
|
||||
}
|
||||
|
||||
bool AXP2101Component::disable_power_rail(PowerRail rail) {
|
||||
uint8_t reg, bit;
|
||||
if (!this->get_rail_enable_info(rail, reg, bit)) {
|
||||
return false;
|
||||
}
|
||||
return this->clear_register_bit(reg, bit);
|
||||
}
|
||||
|
||||
bool AXP2101Component::is_power_rail_enabled(PowerRail rail) {
|
||||
uint8_t reg, bit;
|
||||
if (!this->get_rail_enable_info(rail, reg, bit)) {
|
||||
return false;
|
||||
}
|
||||
return this->get_register_bit(reg, bit);
|
||||
}
|
||||
|
||||
bool AXP2101Component::set_rail_voltage(PowerRail rail, uint16_t millivolts) {
|
||||
uint8_t reg;
|
||||
uint16_t min_mv, max_mv, step_mv;
|
||||
if (!this->get_rail_voltage_info(rail, reg, min_mv, max_mv, step_mv)) {
|
||||
ESP_LOGE(TAG, "Invalid power rail");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t value = this->calculate_voltage_register_value(rail, millivolts);
|
||||
if (value == 0 && millivolts != 0) {
|
||||
ESP_LOGE(TAG, "Invalid voltage %u mV for rail", millivolts);
|
||||
return false;
|
||||
}
|
||||
|
||||
return this->write_byte(reg, value);
|
||||
}
|
||||
|
||||
uint16_t AXP2101Component::get_rail_voltage(PowerRail rail) {
|
||||
uint8_t reg;
|
||||
uint16_t min_mv, max_mv, step_mv;
|
||||
if (!this->get_rail_voltage_info(rail, reg, min_mv, max_mv, step_mv)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t value;
|
||||
if (!this->read_byte(reg, &value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this->calculate_millivolts_from_register(rail, value & 0x7F);
|
||||
}
|
||||
|
||||
uint16_t AXP2101Component::read_battery_voltage() {
|
||||
// 1 LSB = 1mV
|
||||
return this->read_register_h5l8(AXP2101_ADC_DATA_RELUST0, AXP2101_ADC_DATA_RELUST1);
|
||||
}
|
||||
|
||||
uint16_t AXP2101Component::read_vbus_voltage() {
|
||||
// 1 LSB = 1mV
|
||||
return this->read_register_h5l8(AXP2101_ADC_DATA_RELUST4, AXP2101_ADC_DATA_RELUST5);
|
||||
}
|
||||
|
||||
uint16_t AXP2101Component::read_vsys_voltage() {
|
||||
// 1 LSB = 1mV
|
||||
return this->read_register_h6l8(AXP2101_ADC_DATA_RELUST6, AXP2101_ADC_DATA_RELUST7);
|
||||
}
|
||||
|
||||
float AXP2101Component::read_die_temperature() {
|
||||
// Temperature conversion: T = raw * 0.1 - 267.15
|
||||
return (this->read_register_h6l8(AXP2101_ADC_DATA_RELUST8, AXP2101_ADC_DATA_RELUST9) * TEMP_CONVERSION_FACTOR) +
|
||||
TEMP_OFFSET;
|
||||
}
|
||||
|
||||
uint8_t AXP2101Component::read_battery_level() {
|
||||
uint8_t level;
|
||||
if (!this->read_byte(AXP2101_BAT_PERCENT_DATA, &level)) {
|
||||
return 0;
|
||||
}
|
||||
return level > 100 ? 100 : level;
|
||||
}
|
||||
|
||||
|
||||
} // namespace axp2101
|
||||
} // namespace esphome
|
||||
175
esphome/components/axp2101/axp2101.h
Normal file
175
esphome/components/axp2101/axp2101.h
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @file axp2101.h
|
||||
* @brief AXP2101 Power Management IC Component
|
||||
*
|
||||
* This component provides access to the AXP2101 PMIC which includes:
|
||||
* - 5 DCDC regulators (DCDC1-5)
|
||||
* - 11 LDO regulators (ALDO1-4, BLDO1-2, CPUSLDO, DLDO1-2)
|
||||
* - Battery management and charging
|
||||
* - ADC monitoring (battery voltage, current, temperature, etc.)
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace axp2101 {
|
||||
|
||||
// Register addresses
|
||||
static const uint8_t AXP2101_STATUS1 = 0x00;
|
||||
static const uint8_t AXP2101_STATUS2 = 0x01;
|
||||
static const uint8_t AXP2101_IC_TYPE = 0x03;
|
||||
static const uint8_t AXP2101_COMMON_CONFIG = 0x10;
|
||||
static const uint8_t AXP2101_BATFET_CTRL = 0x12;
|
||||
static const uint8_t AXP2101_MIN_SYS_VOL_CTRL = 0x14;
|
||||
static const uint8_t AXP2101_INPUT_VOL_LIMIT_CTRL = 0x15;
|
||||
static const uint8_t AXP2101_INPUT_CUR_LIMIT_CTRL = 0x16;
|
||||
static const uint8_t AXP2101_PWRON_STATUS = 0x20;
|
||||
static const uint8_t AXP2101_PWROFF_STATUS = 0x21;
|
||||
static const uint8_t AXP2101_PWROFF_EN = 0x22;
|
||||
|
||||
// ADC control and data registers
|
||||
static const uint8_t AXP2101_ADC_CHANNEL_CTRL = 0x30;
|
||||
static const uint8_t AXP2101_ADC_DATA_RELUST0 = 0x34; // Battery voltage high
|
||||
static const uint8_t AXP2101_ADC_DATA_RELUST1 = 0x35; // Battery voltage low
|
||||
static const uint8_t AXP2101_ADC_DATA_RELUST2 = 0x36; // TS voltage high
|
||||
static const uint8_t AXP2101_ADC_DATA_RELUST3 = 0x37; // TS voltage low
|
||||
static const uint8_t AXP2101_ADC_DATA_RELUST4 = 0x38; // VBUS voltage high
|
||||
static const uint8_t AXP2101_ADC_DATA_RELUST5 = 0x39; // VBUS voltage low
|
||||
static const uint8_t AXP2101_ADC_DATA_RELUST6 = 0x3A; // VSYS voltage high
|
||||
static const uint8_t AXP2101_ADC_DATA_RELUST7 = 0x3B; // VSYS voltage low
|
||||
static const uint8_t AXP2101_ADC_DATA_RELUST8 = 0x3C; // Die temperature high
|
||||
static const uint8_t AXP2101_ADC_DATA_RELUST9 = 0x3D; // Die temperature low
|
||||
|
||||
// Interrupt registers
|
||||
static const uint8_t AXP2101_INTEN1 = 0x40;
|
||||
static const uint8_t AXP2101_INTEN2 = 0x41;
|
||||
static const uint8_t AXP2101_INTEN3 = 0x42;
|
||||
static const uint8_t AXP2101_INTSTS1 = 0x48;
|
||||
static const uint8_t AXP2101_INTSTS2 = 0x49;
|
||||
static const uint8_t AXP2101_INTSTS3 = 0x4A;
|
||||
|
||||
// Charging configuration registers
|
||||
static const uint8_t AXP2101_CHARGE_GAUGE_WDT_CTRL = 0x18;
|
||||
static const uint8_t AXP2101_IPRECHG_SET = 0x61;
|
||||
static const uint8_t AXP2101_ICC_CHG_SET = 0x62;
|
||||
static const uint8_t AXP2101_ITERM_CHG_SET_CTRL = 0x63;
|
||||
static const uint8_t AXP2101_CV_CHG_VOL_SET = 0x64;
|
||||
static const uint8_t AXP2101_CHARGE_TIMEOUT_SET_CTRL = 0x67;
|
||||
static const uint8_t AXP2101_BAT_DET_CTRL = 0x68;
|
||||
static const uint8_t AXP2101_CHGLED_SET_CTRL = 0x69;
|
||||
static const uint8_t AXP2101_BTN_BAT_CHG_VOL_SET = 0x6A;
|
||||
|
||||
// DCDC control registers
|
||||
static const uint8_t AXP2101_DC_ONOFF_DVM_CTRL = 0x80;
|
||||
static const uint8_t AXP2101_DC_VOL0_CTRL = 0x82; // DCDC1
|
||||
static const uint8_t AXP2101_DC_VOL1_CTRL = 0x83; // DCDC2
|
||||
static const uint8_t AXP2101_DC_VOL2_CTRL = 0x84; // DCDC3
|
||||
static const uint8_t AXP2101_DC_VOL3_CTRL = 0x85; // DCDC4
|
||||
static const uint8_t AXP2101_DC_VOL4_CTRL = 0x86; // DCDC5
|
||||
|
||||
// LDO control registers
|
||||
static const uint8_t AXP2101_LDO_ONOFF_CTRL0 = 0x90;
|
||||
static const uint8_t AXP2101_LDO_ONOFF_CTRL1 = 0x91;
|
||||
static const uint8_t AXP2101_ALDO1_VOL_CTRL = 0x92;
|
||||
static const uint8_t AXP2101_ALDO2_VOL_CTRL = 0x93;
|
||||
static const uint8_t AXP2101_ALDO3_VOL_CTRL = 0x94;
|
||||
static const uint8_t AXP2101_ALDO4_VOL_CTRL = 0x95;
|
||||
static const uint8_t AXP2101_BLDO1_VOL_CTRL = 0x96;
|
||||
static const uint8_t AXP2101_BLDO2_VOL_CTRL = 0x97;
|
||||
static const uint8_t AXP2101_CPUSLDO_VOL_CTRL = 0x98;
|
||||
static const uint8_t AXP2101_DLDO1_VOL_CTRL = 0x99;
|
||||
static const uint8_t AXP2101_DLDO2_VOL_CTRL = 0x9A;
|
||||
|
||||
// Battery gauge registers
|
||||
static const uint8_t AXP2101_BAT_PARAMS = 0xA1;
|
||||
static const uint8_t AXP2101_BAT_GAUGE_CTRL = 0xA2;
|
||||
static const uint8_t AXP2101_BAT_PERCENT_DATA = 0xA4;
|
||||
|
||||
// Power rail identifiers
|
||||
enum PowerRail {
|
||||
DCDC1 = 0,
|
||||
DCDC2,
|
||||
DCDC3,
|
||||
DCDC4,
|
||||
DCDC5,
|
||||
ALDO1,
|
||||
ALDO2,
|
||||
ALDO3,
|
||||
ALDO4,
|
||||
BLDO1,
|
||||
BLDO2,
|
||||
CPUSLDO,
|
||||
DLDO1,
|
||||
DLDO2,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Main AXP2101 component class
|
||||
*
|
||||
* This component handles communication with the AXP2101 PMIC via I2C.
|
||||
* It provides methods for power rail control, voltage adjustment,
|
||||
* and monitoring of various parameters.
|
||||
*/
|
||||
class AXP2101Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// Sensor setters
|
||||
void set_battery_voltage_sensor(sensor::Sensor *sensor) { this->battery_voltage_sensor_ = sensor; }
|
||||
void set_battery_current_sensor(sensor::Sensor *sensor) { this->battery_current_sensor_ = sensor; }
|
||||
void set_battery_level_sensor(sensor::Sensor *sensor) { this->battery_level_sensor_ = sensor; }
|
||||
void set_vbus_voltage_sensor(sensor::Sensor *sensor) { this->vbus_voltage_sensor_ = sensor; }
|
||||
void set_vsys_voltage_sensor(sensor::Sensor *sensor) { this->vsys_voltage_sensor_ = sensor; }
|
||||
void set_die_temperature_sensor(sensor::Sensor *sensor) { this->die_temperature_sensor_ = sensor; }
|
||||
#endif
|
||||
|
||||
// Power rail control methods
|
||||
bool enable_power_rail(PowerRail rail);
|
||||
bool disable_power_rail(PowerRail rail);
|
||||
bool is_power_rail_enabled(PowerRail rail);
|
||||
|
||||
// Voltage control methods
|
||||
bool set_rail_voltage(PowerRail rail, uint16_t millivolts);
|
||||
uint16_t get_rail_voltage(PowerRail rail);
|
||||
|
||||
// ADC reading methods
|
||||
uint16_t read_battery_voltage();
|
||||
uint16_t read_vbus_voltage();
|
||||
uint16_t read_vsys_voltage();
|
||||
float read_die_temperature();
|
||||
uint8_t read_battery_level();
|
||||
|
||||
protected:
|
||||
// Helper methods for register access
|
||||
bool set_register_bit(uint8_t reg, uint8_t bit);
|
||||
bool clear_register_bit(uint8_t reg, uint8_t bit);
|
||||
bool get_register_bit(uint8_t reg, uint8_t bit);
|
||||
uint16_t read_register_h5l8(uint8_t high_reg, uint8_t low_reg);
|
||||
uint16_t read_register_h6l8(uint8_t high_reg, uint8_t low_reg);
|
||||
|
||||
// Power rail helper methods
|
||||
bool get_rail_enable_info(PowerRail rail, uint8_t ®, uint8_t &bit);
|
||||
bool get_rail_voltage_info(PowerRail rail, uint8_t ®, uint16_t &min_mv, uint16_t &max_mv, uint16_t &step_mv);
|
||||
uint8_t calculate_voltage_register_value(PowerRail rail, uint16_t millivolts);
|
||||
uint16_t calculate_millivolts_from_register(PowerRail rail, uint8_t reg_value);
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// Sensor pointers (optional)
|
||||
sensor::Sensor *battery_voltage_sensor_{nullptr};
|
||||
sensor::Sensor *battery_current_sensor_{nullptr};
|
||||
sensor::Sensor *battery_level_sensor_{nullptr};
|
||||
sensor::Sensor *vbus_voltage_sensor_{nullptr};
|
||||
sensor::Sensor *vsys_voltage_sensor_{nullptr};
|
||||
sensor::Sensor *die_temperature_sensor_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace axp2101
|
||||
} // namespace esphome
|
||||
90
esphome/components/axp2101/number/__init__.py
Normal file
90
esphome/components/axp2101/number/__init__.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Number support for AXP2101 voltage control."""
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import number
|
||||
from esphome.const import CONF_ID, UNIT_VOLT
|
||||
|
||||
from .. import AXP2101Component, axp2101_ns
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["axp2101"]
|
||||
|
||||
CONF_AXP2101_ID = "axp2101_id"
|
||||
CONF_POWER_RAIL = "power_rail"
|
||||
|
||||
AXP2101Number = axp2101_ns.class_("AXP2101Number", number.Number, cg.Component)
|
||||
|
||||
# Power rail enum matching the PowerRail enum in C++
|
||||
PowerRail = axp2101_ns.enum("PowerRail")
|
||||
POWER_RAILS = {
|
||||
"DCDC1": PowerRail.DCDC1,
|
||||
"DCDC2": PowerRail.DCDC2,
|
||||
"DCDC3": PowerRail.DCDC3,
|
||||
"DCDC4": PowerRail.DCDC4,
|
||||
"DCDC5": PowerRail.DCDC5,
|
||||
"ALDO1": PowerRail.ALDO1,
|
||||
"ALDO2": PowerRail.ALDO2,
|
||||
"ALDO3": PowerRail.ALDO3,
|
||||
"ALDO4": PowerRail.ALDO4,
|
||||
"BLDO1": PowerRail.BLDO1,
|
||||
"BLDO2": PowerRail.BLDO2,
|
||||
"CPUSLDO": PowerRail.CPUSLDO,
|
||||
"DLDO1": PowerRail.DLDO1,
|
||||
"DLDO2": PowerRail.DLDO2,
|
||||
}
|
||||
|
||||
# Voltage ranges for each power rail (in millivolts)
|
||||
VOLTAGE_RANGES = {
|
||||
"DCDC1": {"min": 1500, "max": 3400, "step": 100},
|
||||
"DCDC2": {"min": 500, "max": 1540, "step": 10},
|
||||
"DCDC3": {"min": 500, "max": 3400, "step": 10},
|
||||
"DCDC4": {"min": 500, "max": 1840, "step": 10},
|
||||
"DCDC5": {"min": 1400, "max": 3700, "step": 100},
|
||||
"ALDO1": {"min": 500, "max": 3500, "step": 100},
|
||||
"ALDO2": {"min": 500, "max": 3500, "step": 100},
|
||||
"ALDO3": {"min": 500, "max": 3500, "step": 100},
|
||||
"ALDO4": {"min": 500, "max": 3500, "step": 100},
|
||||
"BLDO1": {"min": 500, "max": 3500, "step": 100},
|
||||
"BLDO2": {"min": 500, "max": 3500, "step": 100},
|
||||
"CPUSLDO": {"min": 500, "max": 1400, "step": 50},
|
||||
"DLDO1": {"min": 500, "max": 3400, "step": 100},
|
||||
"DLDO2": {"min": 500, "max": 3400, "step": 100},
|
||||
}
|
||||
|
||||
|
||||
def validate_voltage_range(config):
|
||||
"""Validate that voltage is within the allowed range for the power rail."""
|
||||
rail = config[CONF_POWER_RAIL]
|
||||
ranges = VOLTAGE_RANGES[rail]
|
||||
|
||||
# Update the number schema with the correct min/max/step
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
number.number_schema(
|
||||
AXP2101Number,
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_AXP2101_ID): cv.use_id(AXP2101Component),
|
||||
cv.Required(CONF_POWER_RAIL): cv.enum(POWER_RAILS, upper=True),
|
||||
}
|
||||
),
|
||||
validate_voltage_range,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate code for AXP2101 number."""
|
||||
paren = await cg.get_variable(config[CONF_AXP2101_ID])
|
||||
var = await number.new_number(config, min_value=0.5, max_value=3.7, step=0.01)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_parent(paren))
|
||||
cg.add(var.set_power_rail(config[CONF_POWER_RAIL]))
|
||||
|
||||
# Set rail-specific voltage range
|
||||
rail = config[CONF_POWER_RAIL]
|
||||
ranges = VOLTAGE_RANGES[rail]
|
||||
cg.add(var.set_voltage_range(ranges["min"], ranges["max"], ranges["step"]))
|
||||
101
esphome/components/axp2101/number/axp2101_number.cpp
Normal file
101
esphome/components/axp2101/number/axp2101_number.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @file axp2101_number.cpp
|
||||
* @brief Implementation of AXP2101 number component
|
||||
*/
|
||||
#include "axp2101_number.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace axp2101 {
|
||||
|
||||
static const char *const TAG = "axp2101.number";
|
||||
|
||||
static const char *power_rail_to_string(PowerRail rail) {
|
||||
switch (rail) {
|
||||
case DCDC1:
|
||||
return "DCDC1";
|
||||
case DCDC2:
|
||||
return "DCDC2";
|
||||
case DCDC3:
|
||||
return "DCDC3";
|
||||
case DCDC4:
|
||||
return "DCDC4";
|
||||
case DCDC5:
|
||||
return "DCDC5";
|
||||
case ALDO1:
|
||||
return "ALDO1";
|
||||
case ALDO2:
|
||||
return "ALDO2";
|
||||
case ALDO3:
|
||||
return "ALDO3";
|
||||
case ALDO4:
|
||||
return "ALDO4";
|
||||
case BLDO1:
|
||||
return "BLDO1";
|
||||
case BLDO2:
|
||||
return "BLDO2";
|
||||
case CPUSLDO:
|
||||
return "CPUSLDO";
|
||||
case DLDO1:
|
||||
return "DLDO1";
|
||||
case DLDO2:
|
||||
return "DLDO2";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
void AXP2101Number::setup() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Parent not set!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Read current voltage and publish initial state
|
||||
uint16_t current_mv = this->parent_->get_rail_voltage(this->rail_);
|
||||
if (current_mv > 0) {
|
||||
float current_v = current_mv / 1000.0f;
|
||||
this->publish_state(current_v);
|
||||
}
|
||||
}
|
||||
|
||||
void AXP2101Number::dump_config() {
|
||||
LOG_NUMBER("", "AXP2101 Number", this);
|
||||
ESP_LOGCONFIG(TAG, " Power Rail: %s", power_rail_to_string(this->rail_));
|
||||
ESP_LOGCONFIG(TAG, " Voltage Range: %.2f-%.2fV (step: %.3fV)", this->min_mv_ / 1000.0f, this->max_mv_ / 1000.0f,
|
||||
this->step_mv_ / 1000.0f);
|
||||
}
|
||||
|
||||
void AXP2101Number::control(float value) {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Parent not set!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert volts to millivolts
|
||||
uint16_t millivolts = static_cast<uint16_t>(value * 1000.0f);
|
||||
|
||||
// Validate range
|
||||
if (millivolts < this->min_mv_ || millivolts > this->max_mv_) {
|
||||
ESP_LOGW(TAG, "Voltage %.3fV out of range for %s (%.2f-%.2fV)", value, power_rail_to_string(this->rail_),
|
||||
this->min_mv_ / 1000.0f, this->max_mv_ / 1000.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
// Round to nearest step
|
||||
uint16_t offset = millivolts - this->min_mv_;
|
||||
uint16_t steps = (offset + this->step_mv_ / 2) / this->step_mv_;
|
||||
millivolts = this->min_mv_ + (steps * this->step_mv_);
|
||||
|
||||
if (this->parent_->set_rail_voltage(this->rail_, millivolts)) {
|
||||
float actual_v = millivolts / 1000.0f;
|
||||
this->publish_state(actual_v);
|
||||
ESP_LOGD(TAG, "Set %s voltage to %.3fV", power_rail_to_string(this->rail_), actual_v);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to set %s voltage to %.3fV", power_rail_to_string(this->rail_), value);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace axp2101
|
||||
} // namespace esphome
|
||||
44
esphome/components/axp2101/number/axp2101_number.h
Normal file
44
esphome/components/axp2101/number/axp2101_number.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @file axp2101_number.h
|
||||
* @brief Number component for AXP2101 voltage control
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "../axp2101.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace axp2101 {
|
||||
|
||||
/**
|
||||
* @brief Number component for controlling AXP2101 power rail voltages
|
||||
*
|
||||
* This number component allows setting the output voltage of individual
|
||||
* power rails (DCDCs and LDOs).
|
||||
*/
|
||||
class AXP2101Number : public number::Number, public Component {
|
||||
public:
|
||||
void set_parent(AXP2101Component *parent) { this->parent_ = parent; }
|
||||
void set_power_rail(PowerRail rail) { this->rail_ = rail; }
|
||||
void set_voltage_range(uint16_t min_mv, uint16_t max_mv, uint16_t step_mv) {
|
||||
this->min_mv_ = min_mv;
|
||||
this->max_mv_ = max_mv;
|
||||
this->step_mv_ = step_mv;
|
||||
}
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void control(float value) override;
|
||||
|
||||
AXP2101Component *parent_;
|
||||
PowerRail rail_;
|
||||
uint16_t min_mv_{500};
|
||||
uint16_t max_mv_{3500};
|
||||
uint16_t step_mv_{100};
|
||||
};
|
||||
|
||||
} // namespace axp2101
|
||||
} // namespace esphome
|
||||
86
esphome/components/axp2101/sensor.py
Normal file
86
esphome/components/axp2101/sensor.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Sensor support for AXP2101."""
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
|
||||
from . import AXP2101Component, axp2101_ns
|
||||
|
||||
DEPENDENCIES = ["axp2101"]
|
||||
|
||||
CONF_AXP2101_ID = "axp2101_id"
|
||||
CONF_VBUS_VOLTAGE = "vbus_voltage"
|
||||
CONF_VSYS_VOLTAGE = "vsys_voltage"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_AXP2101_ID): cv.use_id(AXP2101Component),
|
||||
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_VBUS_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_VSYS_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate code for AXP2101 sensors."""
|
||||
paren = await cg.get_variable(config[CONF_AXP2101_ID])
|
||||
|
||||
if conf := config.get(CONF_BATTERY_VOLTAGE):
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(paren.set_battery_voltage_sensor(sens))
|
||||
|
||||
if conf := config.get(CONF_BATTERY_LEVEL):
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(paren.set_battery_level_sensor(sens))
|
||||
|
||||
if conf := config.get(CONF_VBUS_VOLTAGE):
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(paren.set_vbus_voltage_sensor(sens))
|
||||
|
||||
if conf := config.get(CONF_VSYS_VOLTAGE):
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(paren.set_vsys_voltage_sensor(sens))
|
||||
|
||||
if conf := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(paren.set_die_temperature_sensor(sens))
|
||||
52
esphome/components/axp2101/switch/__init__.py
Normal file
52
esphome/components/axp2101/switch/__init__.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Switch support for AXP2101 power rail control."""
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import switch
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
from .. import AXP2101Component, axp2101_ns
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
DEPENDENCIES = ["axp2101"]
|
||||
|
||||
CONF_AXP2101_ID = "axp2101_id"
|
||||
CONF_POWER_RAIL = "power_rail"
|
||||
|
||||
AXP2101Switch = axp2101_ns.class_("AXP2101Switch", switch.Switch, cg.Component)
|
||||
|
||||
# Power rail enum matching the PowerRail enum in C++
|
||||
PowerRail = axp2101_ns.enum("PowerRail")
|
||||
POWER_RAILS = {
|
||||
"DCDC1": PowerRail.DCDC1,
|
||||
"DCDC2": PowerRail.DCDC2,
|
||||
"DCDC3": PowerRail.DCDC3,
|
||||
"DCDC4": PowerRail.DCDC4,
|
||||
"DCDC5": PowerRail.DCDC5,
|
||||
"ALDO1": PowerRail.ALDO1,
|
||||
"ALDO2": PowerRail.ALDO2,
|
||||
"ALDO3": PowerRail.ALDO3,
|
||||
"ALDO4": PowerRail.ALDO4,
|
||||
"BLDO1": PowerRail.BLDO1,
|
||||
"BLDO2": PowerRail.BLDO2,
|
||||
"CPUSLDO": PowerRail.CPUSLDO,
|
||||
"DLDO1": PowerRail.DLDO1,
|
||||
"DLDO2": PowerRail.DLDO2,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = switch.switch_schema(AXP2101Switch).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_AXP2101_ID): cv.use_id(AXP2101Component),
|
||||
cv.Required(CONF_POWER_RAIL): cv.enum(POWER_RAILS, upper=True),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate code for AXP2101 switch."""
|
||||
paren = await cg.get_variable(config[CONF_AXP2101_ID])
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_parent(paren))
|
||||
cg.add(var.set_power_rail(config[CONF_POWER_RAIL]))
|
||||
74
esphome/components/axp2101/switch/axp2101_switch.cpp
Normal file
74
esphome/components/axp2101/switch/axp2101_switch.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @file axp2101_switch.cpp
|
||||
* @brief Implementation of AXP2101 switch component
|
||||
*/
|
||||
#include "axp2101_switch.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace axp2101 {
|
||||
|
||||
static const char *const TAG = "axp2101.switch";
|
||||
|
||||
static const char *power_rail_to_string(PowerRail rail) {
|
||||
switch (rail) {
|
||||
case DCDC1:
|
||||
return "DCDC1";
|
||||
case DCDC2:
|
||||
return "DCDC2";
|
||||
case DCDC3:
|
||||
return "DCDC3";
|
||||
case DCDC4:
|
||||
return "DCDC4";
|
||||
case DCDC5:
|
||||
return "DCDC5";
|
||||
case ALDO1:
|
||||
return "ALDO1";
|
||||
case ALDO2:
|
||||
return "ALDO2";
|
||||
case ALDO3:
|
||||
return "ALDO3";
|
||||
case ALDO4:
|
||||
return "ALDO4";
|
||||
case BLDO1:
|
||||
return "BLDO1";
|
||||
case BLDO2:
|
||||
return "BLDO2";
|
||||
case CPUSLDO:
|
||||
return "CPUSLDO";
|
||||
case DLDO1:
|
||||
return "DLDO1";
|
||||
case DLDO2:
|
||||
return "DLDO2";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
void AXP2101Switch::dump_config() {
|
||||
LOG_SWITCH("", "AXP2101 Switch", this);
|
||||
ESP_LOGCONFIG(TAG, " Power Rail: %s", power_rail_to_string(this->rail_));
|
||||
}
|
||||
|
||||
void AXP2101Switch::write_state(bool state) {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Parent not set!");
|
||||
return;
|
||||
}
|
||||
|
||||
bool success;
|
||||
if (state) {
|
||||
success = this->parent_->enable_power_rail(this->rail_);
|
||||
} else {
|
||||
success = this->parent_->disable_power_rail(this->rail_);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
this->publish_state(state);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to %s power rail %s", state ? "enable" : "disable", power_rail_to_string(this->rail_));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace axp2101
|
||||
} // namespace esphome
|
||||
34
esphome/components/axp2101/switch/axp2101_switch.h
Normal file
34
esphome/components/axp2101/switch/axp2101_switch.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @file axp2101_switch.h
|
||||
* @brief Switch component for AXP2101 power rail control
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "../axp2101.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace axp2101 {
|
||||
|
||||
/**
|
||||
* @brief Switch component for controlling AXP2101 power rails
|
||||
*
|
||||
* This switch allows enabling/disabling individual power rails (DCDCs and LDOs).
|
||||
*/
|
||||
class AXP2101Switch : public switch_::Switch, public Component {
|
||||
public:
|
||||
void set_parent(AXP2101Component *parent) { this->parent_ = parent; }
|
||||
void set_power_rail(PowerRail rail) { this->rail_ = rail; }
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
|
||||
AXP2101Component *parent_;
|
||||
PowerRail rail_;
|
||||
};
|
||||
|
||||
} // namespace axp2101
|
||||
} // namespace esphome
|
||||
@@ -1,12 +1,11 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor.automation";
|
||||
|
||||
void binary_sensor::MultiClickTrigger::on_state_(bool state) {
|
||||
void MultiClickTrigger::on_state_(bool state) {
|
||||
// Handle duplicate events
|
||||
if (state == this->last_state_) {
|
||||
return;
|
||||
@@ -67,7 +66,7 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) {
|
||||
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
|
||||
void MultiClickTrigger::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
this->is_in_cooldown_ = true;
|
||||
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
|
||||
@@ -79,7 +78,7 @@ void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
|
||||
this->cancel_timeout("is_valid");
|
||||
this->cancel_timeout("is_not_valid");
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
if (min_length == 0) {
|
||||
this->is_valid_ = true;
|
||||
return;
|
||||
@@ -90,19 +89,19 @@ void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
this->is_valid_ = true;
|
||||
});
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->set_timeout("is_not_valid", max_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
});
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::cancel() {
|
||||
void MultiClickTrigger::cancel() {
|
||||
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::trigger_() {
|
||||
void MultiClickTrigger::trigger_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout("trigger");
|
||||
@@ -118,5 +117,4 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
|
||||
return length >= min_length && length <= max_length;
|
||||
}
|
||||
}
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
struct MultiClickTriggerEvent {
|
||||
bool state;
|
||||
@@ -172,5 +171,4 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
|
||||
BinarySensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor";
|
||||
|
||||
@@ -63,6 +61,4 @@ void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||
}
|
||||
bool BinarySensor::is_status_binary_sensor() const { return false; }
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
|
||||
#include <initializer_list>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
class BinarySensor;
|
||||
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
|
||||
@@ -70,5 +68,4 @@ class BinarySensorInitiallyOff : public BinarySensor {
|
||||
bool has_state() const override { return true; }
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
#include "binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "sensor.filter";
|
||||
|
||||
@@ -132,6 +130,4 @@ optional<bool> SettleFilter::new_value(bool value) {
|
||||
|
||||
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
class BinarySensor;
|
||||
|
||||
@@ -139,6 +137,4 @@ class SettleFilter : public Filter, public Component {
|
||||
bool steady_{true};
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "cover.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
namespace esphome::cover {
|
||||
|
||||
template<typename... Ts> class OpenAction : public Action<Ts...> {
|
||||
public:
|
||||
@@ -131,5 +130,4 @@ class CoverClosedTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
} // namespace esphome::cover
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
namespace esphome::cover {
|
||||
|
||||
static const char *const TAG = "cover";
|
||||
|
||||
@@ -212,5 +211,4 @@ void CoverRestoreState::apply(Cover *cover) {
|
||||
cover->publish_state();
|
||||
}
|
||||
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
} // namespace esphome::cover
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
#include "cover_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
namespace esphome::cover {
|
||||
|
||||
const extern float COVER_OPEN;
|
||||
const extern float COVER_CLOSED;
|
||||
@@ -157,5 +156,4 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
|
||||
ESPPreferenceObject rtc_;
|
||||
};
|
||||
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
} // namespace esphome::cover
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
namespace esphome::cover {
|
||||
|
||||
class CoverTraits {
|
||||
public:
|
||||
@@ -26,5 +25,4 @@ class CoverTraits {
|
||||
bool supports_stop_{false};
|
||||
};
|
||||
|
||||
} // namespace cover
|
||||
} // namespace esphome
|
||||
} // namespace esphome::cover
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace esphome {
|
||||
namespace dashboard_import {
|
||||
|
||||
static std::string g_package_import_url; // NOLINT
|
||||
static const char *g_package_import_url = ""; // NOLINT
|
||||
|
||||
const std::string &get_package_import_url() { return g_package_import_url; }
|
||||
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
|
||||
const char *get_package_import_url() { return g_package_import_url; }
|
||||
void set_package_import_url(const char *url) { g_package_import_url = url; }
|
||||
|
||||
} // namespace dashboard_import
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
namespace dashboard_import {
|
||||
|
||||
const std::string &get_package_import_url();
|
||||
void set_package_import_url(std::string url);
|
||||
const char *get_package_import_url();
|
||||
void set_package_import_url(const char *url);
|
||||
|
||||
} // namespace dashboard_import
|
||||
} // namespace esphome
|
||||
|
||||
@@ -36,7 +36,6 @@ from esphome.const import (
|
||||
CONF_WEIGHT,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.helpers import cpp_string_escape
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -50,7 +49,6 @@ font_ns = cg.esphome_ns.namespace("font")
|
||||
|
||||
Font = font_ns.class_("Font")
|
||||
Glyph = font_ns.class_("Glyph")
|
||||
GlyphData = font_ns.struct("GlyphData")
|
||||
|
||||
CONF_BPP = "bpp"
|
||||
CONF_EXTRAS = "extras"
|
||||
@@ -463,7 +461,7 @@ FONT_SCHEMA = cv.Schema(
|
||||
)
|
||||
),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
|
||||
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(Glyph),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -488,6 +486,8 @@ class GlyphInfo:
|
||||
|
||||
|
||||
def glyph_to_glyphinfo(glyph, font, size, bpp):
|
||||
# Convert to 32 bit unicode codepoint
|
||||
glyph = ord(glyph)
|
||||
scale = 256 // (1 << bpp)
|
||||
if not font.is_scalable:
|
||||
sizes = [pt_to_px(x.size) for x in font.available_sizes]
|
||||
@@ -583,22 +583,15 @@ async def to_code(config):
|
||||
|
||||
# Create the glyph table that points to data in the above array.
|
||||
glyph_initializer = [
|
||||
cg.StructInitializer(
|
||||
GlyphData,
|
||||
(
|
||||
"a_char",
|
||||
cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"),
|
||||
),
|
||||
(
|
||||
"data",
|
||||
cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"),
|
||||
),
|
||||
("advance", x.advance),
|
||||
("offset_x", x.offset_x),
|
||||
("offset_y", x.offset_y),
|
||||
("width", x.width),
|
||||
("height", x.height),
|
||||
)
|
||||
[
|
||||
x.glyph,
|
||||
prog_arr + (y - len(x.bitmap_data)),
|
||||
x.advance,
|
||||
x.offset_x,
|
||||
x.offset_y,
|
||||
x.width,
|
||||
x.height,
|
||||
]
|
||||
for (x, y) in zip(
|
||||
glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args]))
|
||||
)
|
||||
|
||||
@@ -6,133 +6,245 @@
|
||||
|
||||
namespace esphome {
|
||||
namespace font {
|
||||
|
||||
static const char *const TAG = "font";
|
||||
|
||||
const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; }
|
||||
// Compare the char at the string position with this char.
|
||||
// Return true if this char is less than or equal the other.
|
||||
bool Glyph::compare_to(const uint8_t *str) const {
|
||||
// 1 -> this->char_
|
||||
// 2 -> str
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->glyph_data_->a_char[i] == '\0')
|
||||
return true;
|
||||
if (str[i] == '\0')
|
||||
return false;
|
||||
if (this->glyph_data_->a_char[i] > str[i])
|
||||
return false;
|
||||
if (this->glyph_data_->a_char[i] < str[i])
|
||||
return true;
|
||||
#ifdef USE_LVGL_FONT
|
||||
const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
|
||||
auto *fe = (Font *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data_(unicode_letter);
|
||||
if (gd == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
// this should not happen
|
||||
return false;
|
||||
}
|
||||
int Glyph::match_length(const uint8_t *str) const {
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->glyph_data_->a_char[i] == '\0')
|
||||
return i;
|
||||
if (str[i] != this->glyph_data_->a_char[i])
|
||||
return 0;
|
||||
}
|
||||
// this should not happen
|
||||
return 0;
|
||||
}
|
||||
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
|
||||
*x1 = this->glyph_data_->offset_x;
|
||||
*y1 = this->glyph_data_->offset_y;
|
||||
*width = this->glyph_data_->width;
|
||||
*height = this->glyph_data_->height;
|
||||
return gd->data;
|
||||
}
|
||||
|
||||
Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
|
||||
auto *fe = (Font *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data_(unicode_letter);
|
||||
if (gd == nullptr) {
|
||||
return false;
|
||||
}
|
||||
dsc->adv_w = gd->advance;
|
||||
dsc->ofs_x = gd->offset_x;
|
||||
dsc->ofs_y = fe->height_ - gd->height - gd->offset_y - fe->lv_font_.base_line;
|
||||
dsc->box_w = gd->width;
|
||||
dsc->box_h = gd->height;
|
||||
dsc->is_placeholder = 0;
|
||||
dsc->bpp = fe->get_bpp();
|
||||
return true;
|
||||
}
|
||||
|
||||
const Glyph *Font::get_glyph_data_(uint32_t unicode_letter) {
|
||||
if (unicode_letter == this->last_letter_ && this->last_letter_ != 0)
|
||||
return this->last_data_;
|
||||
auto *glyph = this->find_glyph(unicode_letter);
|
||||
if (glyph == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
this->last_data_ = glyph;
|
||||
this->last_letter_ = unicode_letter;
|
||||
return glyph;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Attempt to extract a 32 bit Unicode codepoint from a UTF-8 string.
|
||||
* If successful, return the codepoint and set the length to the number of bytes read.
|
||||
* If the end of the string has been reached and a valid codepoint has not been found, return 0 and set the length to
|
||||
* 0.
|
||||
*
|
||||
* @param utf8_str The input string
|
||||
* @param length Pointer to length storage
|
||||
* @return The extracted code point
|
||||
*/
|
||||
static uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) {
|
||||
// Safely cast to uint8_t* for correct bitwise operations on bytes
|
||||
const uint8_t *current = reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
uint32_t code_point = 0;
|
||||
uint8_t c1 = *current++;
|
||||
|
||||
// check for end of string
|
||||
if (c1 == 0) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- 1-Byte Sequence: 0xxxxxxx (ASCII) ---
|
||||
if (c1 < 0x80) {
|
||||
// Valid ASCII byte.
|
||||
code_point = c1;
|
||||
// Optimization: No need to check for continuation bytes.
|
||||
}
|
||||
// --- 2-Byte Sequence: 110xxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xE0) == 0xC0) {
|
||||
uint8_t c2 = *current++;
|
||||
|
||||
// Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx)
|
||||
if ((c2 & 0xC0) != 0x80) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x1F) << 6;
|
||||
code_point |= (c2 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (2-byte must be > 0x7F)
|
||||
if (code_point <= 0x7F) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xF0) == 0xE0) {
|
||||
uint8_t c2 = *current++;
|
||||
uint8_t c3 = *current++;
|
||||
|
||||
// Error Check 1: Check continuation bytes
|
||||
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x0F) << 12;
|
||||
code_point |= (c2 & 0x3F) << 6;
|
||||
code_point |= (c3 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (3-byte must be > 0x7FF)
|
||||
// Also check for surrogates (0xD800-0xDFFF)
|
||||
if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xF8) == 0xF0) {
|
||||
uint8_t c2 = *current++;
|
||||
uint8_t c3 = *current++;
|
||||
uint8_t c4 = *current++;
|
||||
|
||||
// Error Check 1: Check continuation bytes
|
||||
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x07) << 18;
|
||||
code_point |= (c2 & 0x3F) << 12;
|
||||
code_point |= (c3 & 0x3F) << 6;
|
||||
code_point |= (c4 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (4-byte must be > 0xFFFF)
|
||||
// Also check for valid Unicode range (must be <= 0x10FFFF)
|
||||
if (code_point <= 0xFFFF || code_point > 0x10FFFF) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) ---
|
||||
else {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
*length = current - reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
return code_point;
|
||||
}
|
||||
|
||||
Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
uint8_t bpp)
|
||||
: baseline_(baseline),
|
||||
: glyphs_(ConstVector(data, data_nr)),
|
||||
baseline_(baseline),
|
||||
height_(height),
|
||||
descender_(descender),
|
||||
linegap_(height - baseline - descender),
|
||||
xheight_(xheight),
|
||||
capheight_(capheight),
|
||||
bpp_(bpp) {
|
||||
glyphs_.reserve(data_nr);
|
||||
for (int i = 0; i < data_nr; ++i)
|
||||
glyphs_.emplace_back(&data[i]);
|
||||
#ifdef USE_LVGL_FONT
|
||||
this->lv_font_.dsc = this;
|
||||
this->lv_font_.line_height = this->get_height();
|
||||
this->lv_font_.base_line = this->lv_font_.line_height - this->get_baseline();
|
||||
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
|
||||
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
|
||||
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
|
||||
this->lv_font_.underline_position = -1;
|
||||
this->lv_font_.underline_thickness = 1;
|
||||
#endif
|
||||
}
|
||||
int Font::match_next_glyph(const uint8_t *str, int *match_length) {
|
||||
|
||||
const Glyph *Font::find_glyph(uint32_t codepoint) const {
|
||||
int lo = 0;
|
||||
int hi = this->glyphs_.size() - 1;
|
||||
while (lo != hi) {
|
||||
int mid = (lo + hi + 1) / 2;
|
||||
if (this->glyphs_[mid].compare_to(str)) {
|
||||
if (this->glyphs_[mid].is_less_or_equal(codepoint)) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
*match_length = this->glyphs_[lo].match_length(str);
|
||||
if (*match_length <= 0)
|
||||
return -1;
|
||||
return lo;
|
||||
auto *result = &this->glyphs_[lo];
|
||||
if (result->code_point == codepoint)
|
||||
return result;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#ifdef USE_DISPLAY
|
||||
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
|
||||
*baseline = this->baseline_;
|
||||
*height = this->height_;
|
||||
int i = 0;
|
||||
int min_x = 0;
|
||||
bool has_char = false;
|
||||
int x = 0;
|
||||
while (str[i] != '\0') {
|
||||
int match_length;
|
||||
int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length);
|
||||
if (glyph_n < 0) {
|
||||
for (;;) {
|
||||
size_t length;
|
||||
auto code_point = extract_unicode_codepoint(str, &length);
|
||||
if (length == 0)
|
||||
break;
|
||||
str += length;
|
||||
auto *glyph = this->find_glyph(code_point);
|
||||
if (glyph == nullptr) {
|
||||
// Unknown char, skip
|
||||
if (!this->get_glyphs().empty())
|
||||
x += this->get_glyphs()[0].glyph_data_->advance;
|
||||
i++;
|
||||
if (!this->glyphs_.empty())
|
||||
x += this->glyphs_[0].advance;
|
||||
continue;
|
||||
}
|
||||
|
||||
const Glyph &glyph = this->glyphs_[glyph_n];
|
||||
if (!has_char) {
|
||||
min_x = glyph.glyph_data_->offset_x;
|
||||
min_x = glyph->offset_x;
|
||||
} else {
|
||||
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
|
||||
min_x = std::min(min_x, x + glyph->offset_x);
|
||||
}
|
||||
x += glyph.glyph_data_->advance;
|
||||
x += glyph->advance;
|
||||
|
||||
i += match_length;
|
||||
has_char = true;
|
||||
}
|
||||
*x_offset = min_x;
|
||||
*width = x - min_x;
|
||||
}
|
||||
|
||||
void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) {
|
||||
int i = 0;
|
||||
int x_at = x_start;
|
||||
int scan_x1, scan_y1, scan_width, scan_height;
|
||||
while (text[i] != '\0') {
|
||||
int match_length;
|
||||
int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length);
|
||||
if (glyph_n < 0) {
|
||||
for (;;) {
|
||||
size_t length;
|
||||
auto code_point = extract_unicode_codepoint(text, &length);
|
||||
if (length == 0)
|
||||
break;
|
||||
text += length;
|
||||
auto *glyph = this->find_glyph(code_point);
|
||||
if (glyph == nullptr) {
|
||||
// Unknown char, skip
|
||||
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
|
||||
if (!this->get_glyphs().empty()) {
|
||||
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance;
|
||||
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
|
||||
ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point);
|
||||
if (!this->glyphs_.empty()) {
|
||||
uint8_t glyph_width = this->glyphs_[0].advance;
|
||||
display->rectangle(x_at, y_start, glyph_width, this->height_, color);
|
||||
x_at += glyph_width;
|
||||
}
|
||||
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const Glyph &glyph = this->get_glyphs()[glyph_n];
|
||||
glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
|
||||
|
||||
const uint8_t *data = glyph.glyph_data_->data;
|
||||
const int max_x = x_at + scan_x1 + scan_width;
|
||||
const int max_y = y_start + scan_y1 + scan_height;
|
||||
const uint8_t *data = glyph->data;
|
||||
const int max_x = x_at + glyph->offset_x + glyph->width;
|
||||
const int max_y = y_start + glyph->offset_y + glyph->height;
|
||||
|
||||
uint8_t bitmask = 0;
|
||||
uint8_t pixel_data = 0;
|
||||
@@ -145,10 +257,10 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
auto b_g = (float) background.g;
|
||||
auto b_b = (float) background.b;
|
||||
auto b_w = (float) background.w;
|
||||
for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) {
|
||||
for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) {
|
||||
for (int glyph_y = y_start + glyph->offset_y; glyph_y != max_y; glyph_y++) {
|
||||
for (int glyph_x = x_at + glyph->offset_x; glyph_x != max_x; glyph_x++) {
|
||||
uint8_t pixel = 0;
|
||||
for (int bit_num = 0; bit_num != this->bpp_; bit_num++) {
|
||||
for (uint8_t bit_num = 0; bit_num != this->bpp_; bit_num++) {
|
||||
if (bitmask == 0) {
|
||||
pixel_data = progmem_read_byte(data++);
|
||||
bitmask = 0x80;
|
||||
@@ -168,12 +280,9 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
}
|
||||
}
|
||||
}
|
||||
x_at += glyph.glyph_data_->advance;
|
||||
|
||||
i += match_length;
|
||||
x_at += glyph->advance;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace font
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,14 +6,30 @@
|
||||
#ifdef USE_DISPLAY
|
||||
#include "esphome/components/display/display.h"
|
||||
#endif
|
||||
#ifdef USE_LVGL_FONT
|
||||
#include <lvgl.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace font {
|
||||
|
||||
class Font;
|
||||
|
||||
struct GlyphData {
|
||||
const uint8_t *a_char;
|
||||
class Glyph {
|
||||
public:
|
||||
constexpr Glyph(uint32_t code_point, const uint8_t *data, int advance, int offset_x, int offset_y, int width,
|
||||
int height)
|
||||
: code_point(code_point),
|
||||
data(data),
|
||||
advance(advance),
|
||||
offset_x(offset_x),
|
||||
offset_y(offset_y),
|
||||
width(width),
|
||||
height(height) {}
|
||||
|
||||
bool is_less_or_equal(uint32_t other) const { return this->code_point <= other; }
|
||||
|
||||
const uint32_t code_point;
|
||||
const uint8_t *data;
|
||||
int advance;
|
||||
int offset_x;
|
||||
@@ -22,26 +38,6 @@ struct GlyphData {
|
||||
int height;
|
||||
};
|
||||
|
||||
class Glyph {
|
||||
public:
|
||||
Glyph(const GlyphData *data) : glyph_data_(data) {}
|
||||
|
||||
const uint8_t *get_char() const;
|
||||
|
||||
bool compare_to(const uint8_t *str) const;
|
||||
|
||||
int match_length(const uint8_t *str) const;
|
||||
|
||||
void scan_area(int *x1, int *y1, int *width, int *height) const;
|
||||
|
||||
const GlyphData *get_glyph_data() const { return this->glyph_data_; }
|
||||
|
||||
protected:
|
||||
friend Font;
|
||||
|
||||
const GlyphData *glyph_data_;
|
||||
};
|
||||
|
||||
class Font
|
||||
#ifdef USE_DISPLAY
|
||||
: public display::BaseFont
|
||||
@@ -50,8 +46,8 @@ class Font
|
||||
public:
|
||||
/** Construct the font with the given glyphs.
|
||||
*
|
||||
* @param data A vector of glyphs, must be sorted lexicographically.
|
||||
* @param data_nr The number of glyphs in data.
|
||||
* @param data A list of glyphs, must be sorted lexicographically.
|
||||
* @param data_nr The number of glyphs
|
||||
* @param baseline The y-offset from the top of the text to the baseline.
|
||||
* @param height The y-offset from the top of the text to the bottom.
|
||||
* @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p).
|
||||
@@ -59,10 +55,10 @@ class Font
|
||||
* @param capheight The height of capital letters, usually measured at the "X" glyph.
|
||||
* @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps.
|
||||
*/
|
||||
Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
uint8_t bpp = 1);
|
||||
|
||||
int match_next_glyph(const uint8_t *str, int *match_length);
|
||||
const Glyph *find_glyph(uint32_t codepoint) const;
|
||||
|
||||
#ifdef USE_DISPLAY
|
||||
void print(int x_start, int y_start, display::Display *display, Color color, const char *text,
|
||||
@@ -77,11 +73,14 @@ class Font
|
||||
inline int get_xheight() { return this->xheight_; }
|
||||
inline int get_capheight() { return this->capheight_; }
|
||||
inline int get_bpp() { return this->bpp_; }
|
||||
#ifdef USE_LVGL_FONT
|
||||
const lv_font_t *get_lv_font() const { return &this->lv_font_; }
|
||||
#endif
|
||||
|
||||
const std::vector<Glyph, RAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
|
||||
const ConstVector<Glyph> &get_glyphs() const { return glyphs_; }
|
||||
|
||||
protected:
|
||||
std::vector<Glyph, RAMAllocator<Glyph>> glyphs_;
|
||||
ConstVector<Glyph> glyphs_;
|
||||
int baseline_;
|
||||
int height_;
|
||||
int descender_;
|
||||
@@ -89,6 +88,14 @@ class Font
|
||||
int xheight_;
|
||||
int capheight_;
|
||||
uint8_t bpp_; // bits per pixel
|
||||
#ifdef USE_LVGL_FONT
|
||||
lv_font_t lv_font_{};
|
||||
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter);
|
||||
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next);
|
||||
const Glyph *get_glyph_data_(uint32_t unicode_letter);
|
||||
uint32_t last_letter_{};
|
||||
const Glyph *last_data_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace font
|
||||
|
||||
@@ -31,35 +31,83 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
|
||||
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_LIGHTBULB,
|
||||
),
|
||||
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
@@ -73,7 +121,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
@@ -81,7 +135,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
|
||||
@@ -31,36 +31,84 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
|
||||
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_LIGHTBULB,
|
||||
unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor
|
||||
),
|
||||
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
@@ -74,7 +122,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
@@ -82,7 +136,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
|
||||
@@ -52,15 +52,7 @@ from .schemas import (
|
||||
from .styles import add_top_layer, styles_to_code, theme_to_code
|
||||
from .touchscreens import touchscreen_schema, touchscreens_to_code
|
||||
from .trigger import add_on_boot_triggers, generate_triggers
|
||||
from .types import (
|
||||
FontEngine,
|
||||
IdleTrigger,
|
||||
PlainTrigger,
|
||||
lv_font_t,
|
||||
lv_group_t,
|
||||
lv_style_t,
|
||||
lvgl_ns,
|
||||
)
|
||||
from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns
|
||||
from .widgets import (
|
||||
LvScrActType,
|
||||
Widget,
|
||||
@@ -244,7 +236,6 @@ async def to_code(configs):
|
||||
cg.add_global(lvgl_ns.using)
|
||||
for font in helpers.esphome_fonts_used:
|
||||
await cg.get_variable(font)
|
||||
cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font))
|
||||
default_font = config_0[df.CONF_DEFAULT_FONT]
|
||||
if not lvalid.is_lv_font(default_font):
|
||||
add_define(
|
||||
@@ -256,7 +247,8 @@ async def to_code(configs):
|
||||
type=lv_font_t.operator("ptr").operator("const"),
|
||||
)
|
||||
cg.new_variable(
|
||||
globfont_id, MockObj(await lvalid.lv_font.process(default_font))
|
||||
globfont_id,
|
||||
MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(),
|
||||
)
|
||||
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
|
||||
else:
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#include "lvgl_esphome.h"
|
||||
|
||||
#ifdef USE_LVGL_FONT
|
||||
namespace esphome {
|
||||
namespace lvgl {
|
||||
|
||||
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
|
||||
auto *fe = (FontEngine *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data(unicode_letter);
|
||||
if (gd == nullptr)
|
||||
return nullptr;
|
||||
// esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data);
|
||||
|
||||
return gd->data;
|
||||
}
|
||||
|
||||
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
|
||||
auto *fe = (FontEngine *) font->dsc;
|
||||
const auto *gd = fe->get_glyph_data(unicode_letter);
|
||||
if (gd == nullptr)
|
||||
return false;
|
||||
dsc->adv_w = gd->advance;
|
||||
dsc->ofs_x = gd->offset_x;
|
||||
dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline;
|
||||
dsc->box_w = gd->width;
|
||||
dsc->box_h = gd->height;
|
||||
dsc->is_placeholder = 0;
|
||||
dsc->bpp = fe->bpp;
|
||||
return true;
|
||||
}
|
||||
|
||||
FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) {
|
||||
this->bpp = esp_font->get_bpp();
|
||||
this->lv_font_.dsc = this;
|
||||
this->lv_font_.line_height = this->height = esp_font->get_height();
|
||||
this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline();
|
||||
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
|
||||
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
|
||||
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
|
||||
this->lv_font_.underline_position = -1;
|
||||
this->lv_font_.underline_thickness = 1;
|
||||
}
|
||||
|
||||
const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; }
|
||||
|
||||
const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) {
|
||||
if (unicode_letter == last_letter_)
|
||||
return this->last_data_;
|
||||
uint8_t unicode[5];
|
||||
memset(unicode, 0, sizeof unicode);
|
||||
if (unicode_letter > 0xFFFF) {
|
||||
unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7);
|
||||
unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F);
|
||||
unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F);
|
||||
unicode[3] = 0x80 + (unicode_letter & 0x3F);
|
||||
} else if (unicode_letter > 0x7FF) {
|
||||
unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF);
|
||||
unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F);
|
||||
unicode[2] = 0x80 + (unicode_letter & 0x3F);
|
||||
} else if (unicode_letter > 0x7F) {
|
||||
unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F);
|
||||
unicode[1] = 0x80 + (unicode_letter & 0x3F);
|
||||
} else {
|
||||
unicode[0] = unicode_letter;
|
||||
}
|
||||
int match_length;
|
||||
int glyph_n = this->font_->match_next_glyph(unicode, &match_length);
|
||||
if (glyph_n < 0)
|
||||
return nullptr;
|
||||
this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data();
|
||||
this->last_letter_ = unicode_letter;
|
||||
return this->last_data_;
|
||||
}
|
||||
} // namespace lvgl
|
||||
} // namespace esphome
|
||||
#endif // USES_LVGL_FONT
|
||||
@@ -493,6 +493,7 @@ class LvFont(LValidator):
|
||||
return LV_FONTS
|
||||
if is_lv_font(value):
|
||||
return lv_builtin_font(value)
|
||||
add_lv_use("font")
|
||||
fontval = cv.use_id(Font)(value)
|
||||
esphome_fonts_used.add(fontval)
|
||||
return requires_component("font")(fontval)
|
||||
@@ -502,7 +503,9 @@ class LvFont(LValidator):
|
||||
async def process(self, value, args=()):
|
||||
if is_lv_font(value):
|
||||
return literal(f"&lv_font_{value}")
|
||||
return literal(f"{value}_engine->get_lv_font()")
|
||||
if isinstance(value, str):
|
||||
return literal(f"{value}")
|
||||
return await super().process(value, args)
|
||||
|
||||
|
||||
lv_font = LvFont()
|
||||
|
||||
@@ -50,6 +50,14 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
|
||||
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332;
|
||||
#endif // LV_COLOR_DEPTH
|
||||
|
||||
#ifdef USE_LVGL_FONT
|
||||
inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) {
|
||||
lv_obj_set_style_text_font(obj, font->get_lv_font(), part);
|
||||
}
|
||||
inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
|
||||
lv_style_set_text_font(style, font->get_lv_font());
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
// Shortcut / overload, so that the source of an image can easily be updated
|
||||
// from within a lambda.
|
||||
@@ -134,24 +142,6 @@ template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
|
||||
protected:
|
||||
std::function<void(Ts...)> lamb_;
|
||||
};
|
||||
#ifdef USE_LVGL_FONT
|
||||
class FontEngine {
|
||||
public:
|
||||
FontEngine(font::Font *esp_font);
|
||||
const lv_font_t *get_lv_font();
|
||||
|
||||
const font::GlyphData *get_glyph_data(uint32_t unicode_letter);
|
||||
uint16_t baseline{};
|
||||
uint16_t height{};
|
||||
uint8_t bpp{};
|
||||
|
||||
protected:
|
||||
font::Font *font_{};
|
||||
uint32_t last_letter_{};
|
||||
const font::GlyphData *last_data_{};
|
||||
lv_font_t lv_font_{};
|
||||
};
|
||||
#endif // USE_LVGL_FONT
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
void lv_animimg_stop(lv_obj_t *obj);
|
||||
#endif // USE_LVGL_ANIMIMG
|
||||
|
||||
@@ -45,7 +45,6 @@ lv_coord_t = cg.global_ns.namespace("lv_coord_t")
|
||||
lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
|
||||
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
|
||||
lv_key_t = cg.global_ns.enum("lv_key_t")
|
||||
FontEngine = lvgl_ns.class_("FontEngine")
|
||||
PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template())
|
||||
DrawEndTrigger = esphome_ns.class_(
|
||||
"Trigger<uint32_t, uint32_t>", automation.Trigger.template(cg.uint32, cg.uint32)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome import automation
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
@@ -114,7 +115,9 @@ class SpinboxType(WidgetType):
|
||||
w.obj, digits, digits - config[CONF_DECIMAL_PLACES]
|
||||
)
|
||||
if (value := config.get(CONF_VALUE)) is not None:
|
||||
lv.spinbox_set_value(w.obj, await lv_float.process(value))
|
||||
lv.spinbox_set_value(
|
||||
w.obj, MockObj(await lv_float.process(value)) * w.get_scale()
|
||||
)
|
||||
|
||||
def get_scale(self, config):
|
||||
return 10 ** config[CONF_DECIMAL_PLACES]
|
||||
|
||||
@@ -135,8 +135,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
||||
|
||||
#ifdef USE_DASHBOARD_IMPORT
|
||||
MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
|
||||
txt_records.push_back(
|
||||
{MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url().c_str())});
|
||||
txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url())});
|
||||
#endif
|
||||
}
|
||||
#endif // USE_API
|
||||
|
||||
@@ -350,6 +350,7 @@ void MipiRgb::dump_config() {
|
||||
"\n Width: %u"
|
||||
"\n Height: %u"
|
||||
"\n Rotation: %d degrees"
|
||||
"\n PCLK Inverted: %s"
|
||||
"\n HSync Pulse Width: %u"
|
||||
"\n HSync Back Porch: %u"
|
||||
"\n HSync Front Porch: %u"
|
||||
@@ -357,18 +358,18 @@ void MipiRgb::dump_config() {
|
||||
"\n VSync Back Porch: %u"
|
||||
"\n VSync Front Porch: %u"
|
||||
"\n Invert Colors: %s"
|
||||
"\n Pixel Clock: %dMHz"
|
||||
"\n Pixel Clock: %uMHz"
|
||||
"\n Reset Pin: %s"
|
||||
"\n DE Pin: %s"
|
||||
"\n PCLK Pin: %s"
|
||||
"\n HSYNC Pin: %s"
|
||||
"\n VSYNC Pin: %s",
|
||||
this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_,
|
||||
this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_,
|
||||
this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000,
|
||||
get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(),
|
||||
get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(),
|
||||
get_pin_name(this->vsync_pin_).c_str());
|
||||
this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_),
|
||||
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
|
||||
this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_),
|
||||
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(),
|
||||
get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
|
||||
get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str());
|
||||
|
||||
if (this->madctl_ & MADCTL_BGR) {
|
||||
this->dump_pins_(8, 13, "Blue", 0);
|
||||
|
||||
@@ -11,6 +11,7 @@ st7701s.extend(
|
||||
vsync_pin=17,
|
||||
pclk_pin=21,
|
||||
pclk_frequency="12MHz",
|
||||
pclk_inverted=False,
|
||||
pixel_mode="18bit",
|
||||
mirror_x=True,
|
||||
mirror_y=True,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
namespace esphome::number {
|
||||
|
||||
static const char *const TAG = "number.automation";
|
||||
|
||||
@@ -52,5 +51,4 @@ void ValueRangeTrigger::on_state_(float state) {
|
||||
this->rtc_.save(&in_range);
|
||||
}
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
} // namespace esphome::number
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
namespace esphome::number {
|
||||
|
||||
class NumberStateTrigger : public Trigger<float> {
|
||||
public:
|
||||
@@ -91,5 +90,4 @@ template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...>
|
||||
float max_{NAN};
|
||||
};
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
} // namespace esphome::number
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
namespace esphome::number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
@@ -43,5 +42,4 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
} // namespace esphome::number
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
#include "number_call.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
namespace esphome::number {
|
||||
|
||||
class Number;
|
||||
void log_number(const char *tag, const char *prefix, const char *type, Number *obj);
|
||||
@@ -53,5 +52,4 @@ class Number : public EntityBase {
|
||||
CallbackManager<void(float)> state_callback_;
|
||||
};
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
} // namespace esphome::number
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
#include "number.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
namespace esphome::number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
@@ -125,5 +124,4 @@ void NumberCall::perform() {
|
||||
this->parent_->control(target_value);
|
||||
}
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
} // namespace esphome::number
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
namespace esphome::number {
|
||||
|
||||
class Number;
|
||||
|
||||
@@ -44,5 +43,4 @@ class NumberCall {
|
||||
bool cycle_;
|
||||
};
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
} // namespace esphome::number
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
namespace esphome::number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
} // namespace esphome::number
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
namespace esphome::number {
|
||||
|
||||
enum NumberMode : uint8_t {
|
||||
NUMBER_MODE_AUTO = 0,
|
||||
@@ -35,5 +34,4 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas
|
||||
NumberMode mode_{NUMBER_MODE_AUTO};
|
||||
};
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
} // namespace esphome::number
|
||||
|
||||
@@ -46,14 +46,14 @@ template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts..
|
||||
|
||||
// execute this script using a tuple that contains the arguments
|
||||
void execute_tuple(const std::tuple<Ts...> &tuple) {
|
||||
this->execute_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->execute_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
|
||||
// Internal function to give scripts readable names.
|
||||
void set_name(const LogString *name) { name_ = name; }
|
||||
|
||||
protected:
|
||||
template<size_t... S> void execute_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void execute_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->execute(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
|
||||
auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]);
|
||||
this->queue_front_ = (this->queue_front_ + 1) % queue_capacity;
|
||||
this->trigger_tuple_(*tuple_ptr, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->trigger_tuple_(*tuple_ptr, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
}
|
||||
}
|
||||
|
||||
template<size_t... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->trigger(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
|
||||
|
||||
while (!this->param_queue_.empty()) {
|
||||
auto ¶ms = this->param_queue_.front();
|
||||
this->play_next_tuple_(params, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->play_next_tuple_(params, typename gens<sizeof...(Ts)>::type());
|
||||
this->param_queue_.pop_front();
|
||||
}
|
||||
// Queue is now empty - disable loop until next play_complex
|
||||
@@ -321,7 +321,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
|
||||
}
|
||||
|
||||
protected:
|
||||
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->play_next_(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
from esphome.config_helpers import merge_config
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_SERVERS,
|
||||
CONF_TIME,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
@@ -12,13 +17,74 @@ from esphome.const import (
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
CONF_SNTP = "sntp"
|
||||
|
||||
sntp_ns = cg.esphome_ns.namespace("sntp")
|
||||
SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock)
|
||||
|
||||
DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"]
|
||||
|
||||
|
||||
def _sntp_final_validate(config: ConfigType) -> None:
|
||||
"""Merge multiple SNTP instances into one, similar to OTA merging behavior."""
|
||||
full_conf = fv.full_config.get()
|
||||
time_confs = full_conf.get(CONF_TIME, [])
|
||||
|
||||
sntp_configs: list[ConfigType] = []
|
||||
other_time_configs: list[ConfigType] = []
|
||||
|
||||
for time_conf in time_confs:
|
||||
if time_conf.get(CONF_PLATFORM) == CONF_SNTP:
|
||||
sntp_configs.append(time_conf)
|
||||
else:
|
||||
other_time_configs.append(time_conf)
|
||||
|
||||
if len(sntp_configs) <= 1:
|
||||
return
|
||||
|
||||
# Merge all SNTP configs into the first one
|
||||
merged = sntp_configs[0]
|
||||
for sntp_conf in sntp_configs[1:]:
|
||||
# Validate that IDs are consistent if manually specified
|
||||
if merged[CONF_ID].is_manual and sntp_conf[CONF_ID].is_manual:
|
||||
raise cv.Invalid(
|
||||
f"Found multiple SNTP configurations but {CONF_ID} is inconsistent"
|
||||
)
|
||||
merged = merge_config(merged, sntp_conf)
|
||||
|
||||
# Deduplicate servers while preserving order
|
||||
servers = merged[CONF_SERVERS]
|
||||
unique_servers = list(dict.fromkeys(servers))
|
||||
|
||||
# Warn if we're dropping servers due to 3-server limit
|
||||
if len(unique_servers) > 3:
|
||||
dropped = unique_servers[3:]
|
||||
unique_servers = unique_servers[:3]
|
||||
_LOGGER.warning(
|
||||
"SNTP supports maximum 3 servers. Dropped excess server(s): %s",
|
||||
dropped,
|
||||
)
|
||||
|
||||
merged[CONF_SERVERS] = unique_servers
|
||||
|
||||
_LOGGER.warning(
|
||||
"Found and merged %d SNTP time configurations into one instance",
|
||||
len(sntp_configs),
|
||||
)
|
||||
|
||||
# Replace time configs with merged SNTP + other time platforms
|
||||
other_time_configs.append(merged)
|
||||
full_conf[CONF_TIME] = other_time_configs
|
||||
fv.full_config.set(full_conf)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
time_.TIME_SCHEMA.extend(
|
||||
{
|
||||
@@ -40,6 +106,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _sntp_final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
servers = config[CONF_SERVERS]
|
||||
|
||||
@@ -56,11 +56,19 @@ uint32_t ESP8266UartComponent::get_config() {
|
||||
}
|
||||
|
||||
void ESP8266UartComponent::setup() {
|
||||
if (this->rx_pin_) {
|
||||
this->rx_pin_->setup();
|
||||
}
|
||||
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
|
||||
this->tx_pin_->setup();
|
||||
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
|
||||
if (!pin) {
|
||||
return;
|
||||
}
|
||||
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
|
||||
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
|
||||
pin->setup();
|
||||
}
|
||||
};
|
||||
|
||||
setup_pin_if_needed(this->rx_pin_);
|
||||
if (this->rx_pin_ != this->tx_pin_) {
|
||||
setup_pin_if_needed(this->tx_pin_);
|
||||
}
|
||||
|
||||
// Use Arduino HardwareSerial UARTs if all used pins match the ones
|
||||
|
||||
@@ -133,11 +133,19 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->rx_pin_) {
|
||||
this->rx_pin_->setup();
|
||||
}
|
||||
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
|
||||
this->tx_pin_->setup();
|
||||
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
|
||||
if (!pin) {
|
||||
return;
|
||||
}
|
||||
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
|
||||
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
|
||||
pin->setup();
|
||||
}
|
||||
};
|
||||
|
||||
setup_pin_if_needed(this->rx_pin_);
|
||||
if (this->rx_pin_ != this->tx_pin_) {
|
||||
setup_pin_if_needed(this->tx_pin_);
|
||||
}
|
||||
|
||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||
|
||||
@@ -53,7 +53,7 @@ void LibreTinyUARTComponent::setup() {
|
||||
|
||||
auto shouldFallbackToSoftwareSerial = [&]() -> bool {
|
||||
auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool {
|
||||
return pin && pin->get_flags() & mask != gpio::Flags::FLAG_NONE;
|
||||
return pin && (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE;
|
||||
};
|
||||
if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) ||
|
||||
hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) {
|
||||
|
||||
@@ -52,11 +52,19 @@ uint16_t RP2040UartComponent::get_config() {
|
||||
}
|
||||
|
||||
void RP2040UartComponent::setup() {
|
||||
if (this->rx_pin_) {
|
||||
this->rx_pin_->setup();
|
||||
}
|
||||
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
|
||||
this->tx_pin_->setup();
|
||||
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
|
||||
if (!pin) {
|
||||
return;
|
||||
}
|
||||
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
|
||||
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
|
||||
pin->setup();
|
||||
}
|
||||
};
|
||||
|
||||
setup_pin_if_needed(this->rx_pin_);
|
||||
if (this->rx_pin_ != this->tx_pin_) {
|
||||
setup_pin_if_needed(this->tx_pin_);
|
||||
}
|
||||
|
||||
uint16_t config = get_config();
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
|
||||
from esphome.config_helpers import merge_config
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network", "web_server_base"]
|
||||
@@ -12,6 +19,53 @@ DEPENDENCIES = ["network", "web_server_base"]
|
||||
web_server_ns = cg.esphome_ns.namespace("web_server")
|
||||
WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent)
|
||||
|
||||
|
||||
def _web_server_ota_final_validate(config: ConfigType) -> None:
|
||||
"""Merge multiple web_server OTA instances into one.
|
||||
|
||||
Multiple web_server OTA instances register duplicate HTTP handlers for /update,
|
||||
causing undefined behavior. Merge them into a single instance.
|
||||
"""
|
||||
full_conf = fv.full_config.get()
|
||||
ota_confs = full_conf.get(CONF_OTA, [])
|
||||
|
||||
web_server_ota_configs: list[ConfigType] = []
|
||||
other_ota_configs: list[ConfigType] = []
|
||||
|
||||
for ota_conf in ota_confs:
|
||||
if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER:
|
||||
web_server_ota_configs.append(ota_conf)
|
||||
else:
|
||||
other_ota_configs.append(ota_conf)
|
||||
|
||||
if len(web_server_ota_configs) <= 1:
|
||||
return
|
||||
|
||||
# Merge all web_server OTA configs into the first one
|
||||
merged = web_server_ota_configs[0]
|
||||
for ota_conf in web_server_ota_configs[1:]:
|
||||
# Validate that IDs are consistent if manually specified
|
||||
if (
|
||||
merged[CONF_ID].is_manual
|
||||
and ota_conf[CONF_ID].is_manual
|
||||
and merged[CONF_ID] != ota_conf[CONF_ID]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Found multiple web_server OTA configurations but {CONF_ID} is inconsistent"
|
||||
)
|
||||
merged = merge_config(merged, ota_conf)
|
||||
|
||||
_LOGGER.warning(
|
||||
"Found and merged %d web_server OTA configurations into one instance",
|
||||
len(web_server_ota_configs),
|
||||
)
|
||||
|
||||
# Replace OTA configs with merged web_server + other OTA platforms
|
||||
other_ota_configs.append(merged)
|
||||
full_conf[CONF_OTA] = other_ota_configs
|
||||
fv.full_config.set(full_conf)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -22,6 +76,8 @@ CONFIG_SCHEMA = (
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WEB_SERVER_OTA)
|
||||
async def to_code(config):
|
||||
|
||||
@@ -489,10 +489,18 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
||||
|
||||
void AsyncEventSourceResponse::destroy(void *ptr) {
|
||||
auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr);
|
||||
ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load());
|
||||
// Mark as dead by setting fd to 0 - will be cleaned up in the main loop
|
||||
rsp->fd_.store(0);
|
||||
// Note: We don't delete or remove from set here to avoid race conditions
|
||||
int fd = rsp->fd_.exchange(0); // Atomically get and clear fd
|
||||
|
||||
if (fd > 0) {
|
||||
ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
|
||||
// Immediately shut down the socket to prevent lwIP from delivering more data
|
||||
// This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack
|
||||
// tries to deliver queued data after the session is marked as dead
|
||||
// See: https://github.com/esphome/esphome/issues/11936
|
||||
shutdown(fd, SHUT_RDWR);
|
||||
// Note: We don't close() the socket - httpd owns it and will close it
|
||||
}
|
||||
// Session will be cleaned up in the main loop to avoid race conditions
|
||||
}
|
||||
|
||||
// helper for allowing only unique entries in the queue
|
||||
|
||||
@@ -479,11 +479,14 @@ async def to_code(config):
|
||||
cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE]))
|
||||
if config[CONF_FAST_CONNECT]:
|
||||
cg.add_define("USE_WIFI_FAST_CONNECT")
|
||||
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
|
||||
# passive_scan defaults to false in C++ - only set if true
|
||||
if config[CONF_PASSIVE_SCAN]:
|
||||
cg.add(var.set_passive_scan(True))
|
||||
if CONF_OUTPUT_POWER in config:
|
||||
cg.add(var.set_output_power(config[CONF_OUTPUT_POWER]))
|
||||
|
||||
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
|
||||
# enable_on_boot defaults to true in C++ - only set if false
|
||||
if not config[CONF_ENABLE_ON_BOOT]:
|
||||
cg.add(var.set_enable_on_boot(False))
|
||||
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266WiFi", None)
|
||||
|
||||
@@ -526,7 +526,7 @@ class WiFiComponent : public Component {
|
||||
bool btm_{false};
|
||||
bool rrm_{false};
|
||||
#endif
|
||||
bool enable_on_boot_;
|
||||
bool enable_on_boot_{true};
|
||||
bool got_ipv4_address_{false};
|
||||
bool keep_scan_results_{false};
|
||||
|
||||
|
||||
@@ -338,21 +338,44 @@ def check_replaceme(value):
|
||||
)
|
||||
|
||||
|
||||
def _build_list_index(lst):
|
||||
def _get_item_id(item: Any) -> str | Extend | Remove | None:
|
||||
"""Attempts to get a list item's ID"""
|
||||
if not isinstance(item, dict):
|
||||
return None # not a dict, can't have ID
|
||||
# 1.- Check regular case:
|
||||
# - id: my_id
|
||||
item_id = item.get(CONF_ID)
|
||||
if item_id is None and len(item) == 1:
|
||||
# 2.- Check single-key dict case:
|
||||
# - obj:
|
||||
# id: my_id
|
||||
item = next(iter(item.values()))
|
||||
if isinstance(item, dict):
|
||||
item_id = item.get(CONF_ID)
|
||||
if isinstance(item_id, Extend):
|
||||
# Remove instances of Extend so they don't overwrite the original item when merging:
|
||||
del item[CONF_ID]
|
||||
return item_id
|
||||
|
||||
|
||||
def _build_list_index(
|
||||
lst: list[Any],
|
||||
) -> tuple[
|
||||
OrderedDict[str | Extend | Remove, Any], list[tuple[int, str, Any]], set[str]
|
||||
]:
|
||||
index = OrderedDict()
|
||||
extensions, removals = [], set()
|
||||
for item in lst:
|
||||
for pos, item in enumerate(lst):
|
||||
if item is None:
|
||||
removals.add(None)
|
||||
continue
|
||||
item_id = None
|
||||
if isinstance(item, dict) and (item_id := item.get(CONF_ID)):
|
||||
if isinstance(item_id, Extend):
|
||||
extensions.append(item)
|
||||
continue
|
||||
if isinstance(item_id, Remove):
|
||||
removals.add(item_id.value)
|
||||
continue
|
||||
item_id = _get_item_id(item)
|
||||
if isinstance(item_id, Extend):
|
||||
extensions.append((pos, item_id.value, item))
|
||||
continue
|
||||
if isinstance(item_id, Remove):
|
||||
removals.add(item_id.value)
|
||||
continue
|
||||
if not item_id or item_id in index:
|
||||
# no id or duplicate -> pass through with identity-based key
|
||||
item_id = id(item)
|
||||
@@ -360,7 +383,7 @@ def _build_list_index(lst):
|
||||
return index, extensions, removals
|
||||
|
||||
|
||||
def resolve_extend_remove(value, is_key=None):
|
||||
def resolve_extend_remove(value: Any, is_key: bool = False) -> None:
|
||||
if isinstance(value, ESPLiteralValue):
|
||||
return # do not check inside literal blocks
|
||||
if isinstance(value, list):
|
||||
@@ -368,26 +391,16 @@ def resolve_extend_remove(value, is_key=None):
|
||||
if extensions or removals:
|
||||
# Rebuild the original list after
|
||||
# processing all extensions and removals
|
||||
for item in extensions:
|
||||
item_id = item[CONF_ID].value
|
||||
for pos, item_id, item in extensions:
|
||||
if item_id in removals:
|
||||
continue
|
||||
old = index.get(item_id)
|
||||
if old is None:
|
||||
# Failed to find source for extension
|
||||
# Find index of item to show error at correct position
|
||||
i = next(
|
||||
(
|
||||
i
|
||||
for i, d in enumerate(value)
|
||||
if d.get(CONF_ID) == item[CONF_ID]
|
||||
)
|
||||
)
|
||||
with cv.prepend_path(i):
|
||||
with cv.prepend_path(pos):
|
||||
raise cv.Invalid(
|
||||
f"Source for extension of ID '{item_id}' was not found."
|
||||
)
|
||||
item[CONF_ID] = item_id
|
||||
index[item_id] = merge_config(old, item)
|
||||
for item_id in removals:
|
||||
index.pop(item_id, None)
|
||||
|
||||
@@ -336,6 +336,7 @@ CONF_ENERGY = "energy"
|
||||
CONF_ENTITY_CATEGORY = "entity_category"
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_ENUM_DATAPOINT = "enum_datapoint"
|
||||
CONF_ENVIRONMENT_VARIABLES = "environment_variables"
|
||||
CONF_EQUATION = "equation"
|
||||
CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support"
|
||||
CONF_ESPHOME = "esphome"
|
||||
|
||||
@@ -11,26 +11,10 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// C++20 std::index_sequence is now used for tuple unpacking
|
||||
// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility
|
||||
// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
|
||||
// Remove before 2026.6.0
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
|
||||
template<int...> struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {};
|
||||
template<int N, int... S>
|
||||
struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens
|
||||
: gens<N - 1, N - 1, S...> {};
|
||||
template<int... S> struct gens<0, S...> { using type = seq<S...>; };
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
template<int...> struct seq {}; // NOLINT
|
||||
template<int N, int... S> struct gens : gens<N - 1, N - 1, S...> {}; // NOLINT
|
||||
template<int... S> struct gens<0, S...> { using type = seq<S...>; }; // NOLINT
|
||||
|
||||
#define TEMPLATABLE_VALUE_(type, name) \
|
||||
protected: \
|
||||
@@ -168,11 +152,11 @@ template<typename... Ts> class Condition {
|
||||
|
||||
/// Call check with a tuple of values as parameter.
|
||||
bool check_tuple(const std::tuple<Ts...> &tuple) {
|
||||
return this->check_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return this->check_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
|
||||
protected:
|
||||
template<size_t... S> bool check_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> bool check_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
return this->check(std::get<S>(tuple)...);
|
||||
}
|
||||
};
|
||||
@@ -247,11 +231,11 @@ template<typename... Ts> class Action {
|
||||
}
|
||||
}
|
||||
}
|
||||
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->play_next_(std::get<S>(tuple)...);
|
||||
}
|
||||
void play_next_tuple_(const std::tuple<Ts...> &tuple) {
|
||||
this->play_next_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->play_next_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
|
||||
virtual void stop() {}
|
||||
@@ -293,9 +277,7 @@ template<typename... Ts> class ActionList {
|
||||
if (this->actions_begin_ != nullptr)
|
||||
this->actions_begin_->play_complex(x...);
|
||||
}
|
||||
void play_tuple(const std::tuple<Ts...> &tuple) {
|
||||
this->play_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
}
|
||||
void play_tuple(const std::tuple<Ts...> &tuple) { this->play_tuple_(tuple, typename gens<sizeof...(Ts)>::type()); }
|
||||
void stop() {
|
||||
if (this->actions_begin_ != nullptr)
|
||||
this->actions_begin_->stop_complex();
|
||||
@@ -316,7 +298,7 @@ template<typename... Ts> class ActionList {
|
||||
}
|
||||
|
||||
protected:
|
||||
template<size_t... S> void play_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void play_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->play(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from esphome.const import (
|
||||
CONF_COMPILE_PROCESS_LIMIT,
|
||||
CONF_DEBUG_SCHEDULER,
|
||||
CONF_DEVICES,
|
||||
CONF_ENVIRONMENT_VARIABLES,
|
||||
CONF_ESPHOME,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_ID,
|
||||
@@ -215,6 +216,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.string_strict: cv.Any([cv.string], cv.string),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ENVIRONMENT_VARIABLES, default={}): cv.Schema(
|
||||
{
|
||||
cv.string_strict: cv.string,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_BOOT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger),
|
||||
@@ -426,6 +432,12 @@ async def _add_platformio_options(pio_options):
|
||||
cg.add_platformio_option(key, val)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _add_environment_variables(env_vars: dict[str, str]) -> None:
|
||||
# Set environment variables for the build process
|
||||
os.environ.update(env_vars)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _add_automations(config):
|
||||
for conf in config.get(CONF_ON_BOOT, []):
|
||||
@@ -563,6 +575,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config[CONF_PLATFORMIO_OPTIONS]:
|
||||
CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS])
|
||||
|
||||
if config[CONF_ENVIRONMENT_VARIABLES]:
|
||||
CORE.add_job(_add_environment_variables, config[CONF_ENVIRONMENT_VARIABLES])
|
||||
|
||||
# Process areas
|
||||
all_areas: list[dict[str, str | core.ID]] = []
|
||||
if CONF_AREA in config:
|
||||
|
||||
@@ -111,6 +111,23 @@ template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n);
|
||||
/// @name Container utilities
|
||||
///@{
|
||||
|
||||
/// Lightweight read-only view over a const array stored in RODATA (will typically be in flash memory)
|
||||
/// Avoids copying data from flash to RAM by keeping a pointer to the flash data.
|
||||
/// Similar to std::span but with minimal overhead for embedded systems.
|
||||
|
||||
template<typename T> class ConstVector {
|
||||
public:
|
||||
constexpr ConstVector(const T *data, size_t size) : data_(data), size_(size) {}
|
||||
|
||||
const constexpr T &operator[](size_t i) const { return data_[i]; }
|
||||
constexpr size_t size() const { return size_; }
|
||||
constexpr bool empty() const { return size_ == 0; }
|
||||
|
||||
protected:
|
||||
const T *data_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
/// Minimal static vector - saves memory by avoiding std::vector overhead
|
||||
template<typename T, size_t N> class StaticVector {
|
||||
public:
|
||||
|
||||
@@ -609,13 +609,12 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
|
||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||
this->millis_major_++;
|
||||
major++;
|
||||
this->last_millis_ = now;
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||
}
|
||||
|
||||
// Only update if time moved forward
|
||||
if (now > last) {
|
||||
} else if (now > last) {
|
||||
// Only update if time moved forward
|
||||
this->last_millis_ = now;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ def update_storage_json() -> None:
|
||||
)
|
||||
else:
|
||||
_LOGGER.info("Core config or version changed, cleaning build files...")
|
||||
clean_build()
|
||||
clean_build(clear_pio_cache=False)
|
||||
elif storage_should_update_cmake_cache(old, new):
|
||||
_LOGGER.info("Integrations changed, cleaning cmake cache...")
|
||||
clean_cmake_cache()
|
||||
@@ -301,7 +301,7 @@ def clean_cmake_cache():
|
||||
pioenvs_cmake_path.unlink()
|
||||
|
||||
|
||||
def clean_build():
|
||||
def clean_build(clear_pio_cache: bool = True):
|
||||
import shutil
|
||||
|
||||
# Allow skipping cache cleaning for integration tests
|
||||
@@ -322,6 +322,9 @@ def clean_build():
|
||||
_LOGGER.info("Deleting %s", dependencies_lock)
|
||||
dependencies_lock.unlink()
|
||||
|
||||
if not clear_pio_cache:
|
||||
return
|
||||
|
||||
# Clean PlatformIO cache to resolve CMake compiler detection issues
|
||||
# This helps when toolchain paths change or get corrupted
|
||||
try:
|
||||
|
||||
@@ -16,7 +16,7 @@ aioesphomeapi==42.7.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.16 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.14 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==11.3.0
|
||||
cairosvg==2.8.2
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
"""Tests for the web_server OTA platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.web_server.ota import _web_server_ota_final_validate
|
||||
from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER
|
||||
from esphome.core import ID
|
||||
import esphome.final_validate as fv
|
||||
|
||||
|
||||
def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
|
||||
@@ -100,3 +112,144 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None:
|
||||
# Check web server OTA component is present
|
||||
assert "WebServerOTAComponent" in main_cpp
|
||||
assert "web_server::WebServerOTAComponent" in main_cpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("ota_configs", "expected_count", "warning_expected"),
|
||||
[
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web", is_manual=False),
|
||||
}
|
||||
],
|
||||
1,
|
||||
False,
|
||||
id="single_instance_no_merge",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_1", is_manual=False),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_2", is_manual=False),
|
||||
},
|
||||
],
|
||||
1,
|
||||
True,
|
||||
id="two_instances_merged",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_1", is_manual=False),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: "esphome",
|
||||
CONF_ID: ID("ota_esphome", is_manual=False),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_2", is_manual=False),
|
||||
},
|
||||
],
|
||||
2,
|
||||
True,
|
||||
id="mixed_platforms_web_server_merged",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_web_server_ota_instance_merging(
|
||||
ota_configs: list[dict[str, Any]],
|
||||
expected_count: int,
|
||||
warning_expected: bool,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test web_server OTA instance merging behavior."""
|
||||
full_conf = {CONF_OTA: ota_configs.copy()}
|
||||
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_web_server_ota_final_validate({})
|
||||
|
||||
updated_conf = fv.full_config.get()
|
||||
|
||||
# Verify total number of OTA platforms
|
||||
assert len(updated_conf[CONF_OTA]) == expected_count
|
||||
|
||||
# Verify warning
|
||||
if warning_expected:
|
||||
assert any(
|
||||
"Found and merged" in record.message
|
||||
and "web_server OTA" in record.message
|
||||
for record in caplog.records
|
||||
), "Expected merge warning not found in log"
|
||||
else:
|
||||
assert len(caplog.records) == 0, "Unexpected warnings logged"
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_web_server_ota_consistent_manual_ids(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that consistent manual IDs can be merged successfully."""
|
||||
ota_configs = [
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web", is_manual=True),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web", is_manual=True),
|
||||
},
|
||||
]
|
||||
|
||||
full_conf = {CONF_OTA: ota_configs}
|
||||
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_web_server_ota_final_validate({})
|
||||
|
||||
updated_conf = fv.full_config.get()
|
||||
assert len(updated_conf[CONF_OTA]) == 1
|
||||
assert updated_conf[CONF_OTA][0][CONF_ID].id == "ota_web"
|
||||
assert any(
|
||||
"Found and merged" in record.message and "web_server OTA" in record.message
|
||||
for record in caplog.records
|
||||
)
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_web_server_ota_inconsistent_manual_ids() -> None:
|
||||
"""Test that inconsistent manual IDs raise an error."""
|
||||
ota_configs = [
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_1", is_manual=True),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_WEB_SERVER,
|
||||
CONF_ID: ID("ota_web_2", is_manual=True),
|
||||
},
|
||||
]
|
||||
|
||||
full_conf = {CONF_OTA: ota_configs}
|
||||
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match="Found multiple web_server OTA configurations but id is inconsistent",
|
||||
):
|
||||
_web_server_ota_final_validate({})
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
1
tests/component_tests/sntp/__init__.py
Normal file
1
tests/component_tests/sntp/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for SNTP component."""
|
||||
22
tests/component_tests/sntp/config/sntp_test.yaml
Normal file
22
tests/component_tests/sntp/config/sntp_test.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
esphome:
|
||||
name: sntp-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "testssid"
|
||||
password: "testpassword"
|
||||
|
||||
# Test multiple SNTP instances that should be merged
|
||||
time:
|
||||
- platform: sntp
|
||||
servers:
|
||||
- 192.168.1.1
|
||||
- pool.ntp.org
|
||||
- platform: sntp
|
||||
servers:
|
||||
- pool.ntp.org
|
||||
- 192.168.1.2
|
||||
238
tests/component_tests/sntp/test_init.py
Normal file
238
tests/component_tests/sntp/test_init.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""Tests for SNTP time configuration validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate
|
||||
from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME
|
||||
from esphome.core import ID
|
||||
import esphome.final_validate as fv
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("time_configs", "expected_count", "expected_servers", "warning_messages"),
|
||||
[
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
|
||||
}
|
||||
],
|
||||
1,
|
||||
["192.168.1.1", "pool.ntp.org"],
|
||||
[],
|
||||
id="single_instance_no_merge",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.2"],
|
||||
},
|
||||
],
|
||||
1,
|
||||
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
|
||||
["Found and merged 2 SNTP time configurations into one instance"],
|
||||
id="two_instances_merged",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=False),
|
||||
CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"],
|
||||
},
|
||||
],
|
||||
1,
|
||||
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
|
||||
["Found and merged 2 SNTP time configurations into one instance"],
|
||||
id="deduplication_preserves_order",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_3", is_manual=False),
|
||||
CONF_SERVERS: ["pool3.ntp.org"],
|
||||
},
|
||||
],
|
||||
1,
|
||||
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
|
||||
[
|
||||
"SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']",
|
||||
"Found and merged 3 SNTP time configurations into one instance",
|
||||
],
|
||||
id="three_instances_drops_excess_servers",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=False),
|
||||
CONF_SERVERS: [
|
||||
"192.168.1.1",
|
||||
"pool.ntp.org",
|
||||
"pool.ntp.org",
|
||||
"192.168.1.1",
|
||||
],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=False),
|
||||
CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"],
|
||||
},
|
||||
],
|
||||
1,
|
||||
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
|
||||
["Found and merged 2 SNTP time configurations into one instance"],
|
||||
id="deduplication_multiple_duplicates",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_sntp_instance_merging(
|
||||
time_configs: list[dict[str, Any]],
|
||||
expected_count: int,
|
||||
expected_servers: list[str],
|
||||
warning_messages: list[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test SNTP instance merging behavior."""
|
||||
# Create a mock full config with time configs
|
||||
full_conf = {CONF_TIME: time_configs.copy()}
|
||||
|
||||
# Set the context var
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_sntp_final_validate({})
|
||||
|
||||
# Get the updated config
|
||||
updated_conf = fv.full_config.get()
|
||||
|
||||
# Check if merging occurred
|
||||
if len(time_configs) > 1:
|
||||
# Verify only one SNTP instance remains
|
||||
sntp_instances = [
|
||||
tc
|
||||
for tc in updated_conf[CONF_TIME]
|
||||
if tc.get(CONF_PLATFORM) == CONF_SNTP
|
||||
]
|
||||
assert len(sntp_instances) == expected_count
|
||||
|
||||
# Verify server list
|
||||
assert sntp_instances[0][CONF_SERVERS] == expected_servers
|
||||
|
||||
# Verify warnings
|
||||
for expected_msg in warning_messages:
|
||||
assert any(
|
||||
expected_msg in record.message for record in caplog.records
|
||||
), f"Expected warning message '{expected_msg}' not found in log"
|
||||
else:
|
||||
# Single instance should not trigger merging or warnings
|
||||
assert len(caplog.records) == 0
|
||||
# Config should be unchanged
|
||||
assert updated_conf[CONF_TIME] == time_configs
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_sntp_inconsistent_manual_ids() -> None:
|
||||
"""Test that inconsistent manual IDs raise an error."""
|
||||
# Create configs with manual IDs that are inconsistent
|
||||
time_configs = [
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=True),
|
||||
CONF_SERVERS: ["192.168.1.1"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=True),
|
||||
CONF_SERVERS: ["192.168.1.2"],
|
||||
},
|
||||
]
|
||||
|
||||
full_conf = {CONF_TIME: time_configs}
|
||||
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match="Found multiple SNTP configurations but id is inconsistent",
|
||||
):
|
||||
_sntp_final_validate({})
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Test that SNTP merging doesn't affect other time platforms."""
|
||||
time_configs = [
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_1", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.1"],
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: "homeassistant",
|
||||
CONF_ID: ID("homeassistant_time", is_manual=False),
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: CONF_SNTP,
|
||||
CONF_ID: ID("sntp_time_2", is_manual=False),
|
||||
CONF_SERVERS: ["192.168.1.2"],
|
||||
},
|
||||
]
|
||||
|
||||
full_conf = {CONF_TIME: time_configs.copy()}
|
||||
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_sntp_final_validate({})
|
||||
|
||||
updated_conf = fv.full_config.get()
|
||||
|
||||
# Should have 2 time platforms: 1 merged SNTP + 1 homeassistant
|
||||
assert len(updated_conf[CONF_TIME]) == 2
|
||||
|
||||
# Find the platforms
|
||||
platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]}
|
||||
assert platforms == {CONF_SNTP, "homeassistant"}
|
||||
|
||||
# Verify SNTP was merged
|
||||
sntp_instances = [
|
||||
tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP
|
||||
]
|
||||
assert len(sntp_instances) == 1
|
||||
assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"]
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
51
tests/components/axp2101/common.yaml
Normal file
51
tests/components/axp2101/common.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
axp2101:
|
||||
- id: pmic
|
||||
i2c_id: i2c_bus
|
||||
address: 0x34
|
||||
update_interval: 60s
|
||||
|
||||
sensor:
|
||||
- platform: axp2101
|
||||
axp2101_id: pmic
|
||||
battery_voltage:
|
||||
name: "Battery Voltage"
|
||||
battery_level:
|
||||
name: "Battery Level"
|
||||
vbus_voltage:
|
||||
name: "VBUS Voltage"
|
||||
vsys_voltage:
|
||||
name: "VSYS Voltage"
|
||||
temperature:
|
||||
name: "Die Temperature"
|
||||
|
||||
switch:
|
||||
- platform: axp2101
|
||||
axp2101_id: pmic
|
||||
name: "DCDC1 Enable"
|
||||
power_rail: DCDC1
|
||||
- platform: axp2101
|
||||
axp2101_id: pmic
|
||||
name: "ALDO1 Enable"
|
||||
power_rail: ALDO1
|
||||
- platform: axp2101
|
||||
axp2101_id: pmic
|
||||
name: "BLDO1 Enable"
|
||||
power_rail: BLDO1
|
||||
|
||||
number:
|
||||
- platform: axp2101
|
||||
axp2101_id: pmic
|
||||
name: "DCDC1 Voltage"
|
||||
power_rail: DCDC1
|
||||
- platform: axp2101
|
||||
axp2101_id: pmic
|
||||
name: "DCDC2 Voltage"
|
||||
power_rail: DCDC2
|
||||
- platform: axp2101
|
||||
axp2101_id: pmic
|
||||
name: "ALDO1 Voltage"
|
||||
power_rail: ALDO1
|
||||
- platform: axp2101
|
||||
axp2101_id: pmic
|
||||
name: "CPUSLDO Voltage"
|
||||
power_rail: CPUSLDO
|
||||
4
tests/components/axp2101/test.esp32-ard.yaml
Normal file
4
tests/components/axp2101/test.esp32-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/axp2101/test.esp32-idf.yaml
Normal file
4
tests/components/axp2101/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/axp2101/test.esp8266-ard.yaml
Normal file
4
tests/components/axp2101/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/axp2101/test.rp2040-ard.yaml
Normal file
4
tests/components/axp2101/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -2,6 +2,9 @@ esphome:
|
||||
debug_scheduler: true
|
||||
platformio_options:
|
||||
board_build.flash_mode: dio
|
||||
environment_variables:
|
||||
TEST_ENV_VAR: "test_value"
|
||||
BUILD_NUMBER: "12345"
|
||||
area:
|
||||
id: testing_area
|
||||
name: Testing Area
|
||||
|
||||
@@ -76,7 +76,7 @@ lvgl:
|
||||
line_width: 8
|
||||
line_rounded: true
|
||||
- id: date_style
|
||||
text_font: roboto10
|
||||
text_font: !lambda return id(roboto10);
|
||||
align: center
|
||||
text_color: !lambda return color_id2;
|
||||
bg_opa: cover
|
||||
@@ -267,7 +267,7 @@ lvgl:
|
||||
snprintf(buf, sizeof(buf), "Setup: %d", 42);
|
||||
return std::string(buf);
|
||||
align: top_mid
|
||||
text_font: space16
|
||||
text_font: !lambda return id(space16);
|
||||
- label:
|
||||
id: chip_info_label
|
||||
# Test complex setup lambda (real-world pattern)
|
||||
@@ -703,7 +703,9 @@ lvgl:
|
||||
on_value:
|
||||
- lvgl.spinbox.update:
|
||||
id: spinbox_id
|
||||
value: !lambda return x;
|
||||
value: !lambda |-
|
||||
static float yyy = 83.0;
|
||||
return yyy + .8;
|
||||
- button:
|
||||
styles: spin_button
|
||||
id: spin_up
|
||||
|
||||
@@ -18,6 +18,7 @@ touchscreen:
|
||||
|
||||
lvgl:
|
||||
- id: lvgl_0
|
||||
default_font: space16
|
||||
displays: sdl0
|
||||
- id: lvgl_1
|
||||
displays: sdl1
|
||||
@@ -39,3 +40,8 @@ lvgl:
|
||||
text: Click ME
|
||||
on_click:
|
||||
logger.log: Clicked
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: space16
|
||||
bpp: 4
|
||||
|
||||
150
tests/integration/fixtures/sensor_timeout_filter.yaml
Normal file
150
tests/integration/fixtures/sensor_timeout_filter.yaml
Normal file
@@ -0,0 +1,150 @@
|
||||
esphome:
|
||||
name: test-timeout-filters
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# Template sensors that we'll use to publish values
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "Source Timeout Last"
|
||||
id: source_timeout_last
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Timeout Reset"
|
||||
id: source_timeout_reset
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Timeout Static"
|
||||
id: source_timeout_static
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Timeout Lambda"
|
||||
id: source_timeout_lambda
|
||||
accuracy_decimals: 1
|
||||
|
||||
# Test 1: TimeoutFilter - "last" mode (outputs last received value)
|
||||
- platform: copy
|
||||
source_id: source_timeout_last
|
||||
name: "Timeout Last Sensor"
|
||||
id: timeout_last_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: last # Explicitly specify "last" mode to use TimeoutFilter class
|
||||
|
||||
# Test 2: TimeoutFilter - reset behavior (same filter, different source)
|
||||
- platform: copy
|
||||
source_id: source_timeout_reset
|
||||
name: "Timeout Reset Sensor"
|
||||
id: timeout_reset_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: last # Explicitly specify "last" mode
|
||||
|
||||
# Test 3: TimeoutFilterConfigured - static value mode
|
||||
- platform: copy
|
||||
source_id: source_timeout_static
|
||||
name: "Timeout Static Sensor"
|
||||
id: timeout_static_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: 99.9
|
||||
|
||||
# Test 4: TimeoutFilterConfigured - lambda mode
|
||||
- platform: copy
|
||||
source_id: source_timeout_lambda
|
||||
name: "Timeout Lambda Sensor"
|
||||
id: timeout_lambda_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: !lambda "return -1.0;"
|
||||
|
||||
# Scripts to publish values with controlled timing
|
||||
script:
|
||||
# Test 1: Single value followed by timeout
|
||||
- id: test_timeout_last_script
|
||||
then:
|
||||
# Publish initial value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_last
|
||||
state: 42.0
|
||||
# Wait for timeout to fire (100ms + margin)
|
||||
- delay: 150ms
|
||||
|
||||
# Test 2: Multiple values before timeout (should reset timer)
|
||||
- id: test_timeout_reset_script
|
||||
then:
|
||||
# Publish first value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_reset
|
||||
state: 10.0
|
||||
# Wait 50ms (halfway to timeout)
|
||||
- delay: 50ms
|
||||
# Publish second value (resets timeout)
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_reset
|
||||
state: 20.0
|
||||
# Wait 50ms (halfway to timeout again)
|
||||
- delay: 50ms
|
||||
# Publish third value (resets timeout)
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_reset
|
||||
state: 30.0
|
||||
# Wait for timeout to fire (100ms + margin)
|
||||
- delay: 150ms
|
||||
|
||||
# Test 3: Static value timeout
|
||||
- id: test_timeout_static_script
|
||||
then:
|
||||
# Publish initial value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_static
|
||||
state: 55.5
|
||||
# Wait for timeout to fire
|
||||
- delay: 150ms
|
||||
|
||||
# Test 4: Lambda value timeout
|
||||
- id: test_timeout_lambda_script
|
||||
then:
|
||||
# Publish initial value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_lambda
|
||||
state: 77.7
|
||||
# Wait for timeout to fire
|
||||
- delay: 150ms
|
||||
|
||||
# Buttons to trigger each test scenario
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test Timeout Last Button"
|
||||
id: test_timeout_last_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_last_script
|
||||
|
||||
- platform: template
|
||||
name: "Test Timeout Reset Button"
|
||||
id: test_timeout_reset_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_reset_script
|
||||
|
||||
- platform: template
|
||||
name: "Test Timeout Static Button"
|
||||
id: test_timeout_static_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_static_script
|
||||
|
||||
- platform: template
|
||||
name: "Test Timeout Lambda Button"
|
||||
id: test_timeout_lambda_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_lambda_script
|
||||
185
tests/integration/test_sensor_timeout_filter.py
Normal file
185
tests/integration/test_sensor_timeout_filter.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""Test sensor timeout filter functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import EntityState, SensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, build_key_to_entity_mapping
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_timeout_filter(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test TimeoutFilter and TimeoutFilterConfigured with all modes."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track state changes for all sensors
|
||||
timeout_last_states: list[float] = []
|
||||
timeout_reset_states: list[float] = []
|
||||
timeout_static_states: list[float] = []
|
||||
timeout_lambda_states: list[float] = []
|
||||
|
||||
# Futures for each test scenario
|
||||
test1_complete = loop.create_future() # TimeoutFilter - last mode
|
||||
test2_complete = loop.create_future() # TimeoutFilter - reset behavior
|
||||
test3_complete = loop.create_future() # TimeoutFilterConfigured - static value
|
||||
test4_complete = loop.create_future() # TimeoutFilterConfigured - lambda
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
"""Track sensor state updates."""
|
||||
if not isinstance(state, SensorState):
|
||||
return
|
||||
|
||||
if state.missing_state:
|
||||
return
|
||||
|
||||
sensor_name = key_to_sensor.get(state.key)
|
||||
|
||||
# Test 1: TimeoutFilter - last mode
|
||||
if sensor_name == "timeout_last_sensor":
|
||||
timeout_last_states.append(state.state)
|
||||
# Expect 2 values: initial 42.0 + timeout fires with 42.0
|
||||
if len(timeout_last_states) >= 2 and not test1_complete.done():
|
||||
test1_complete.set_result(True)
|
||||
|
||||
# Test 2: TimeoutFilter - reset behavior
|
||||
elif sensor_name == "timeout_reset_sensor":
|
||||
timeout_reset_states.append(state.state)
|
||||
# Expect 4 values: 10.0, 20.0, 30.0, then timeout fires with 30.0
|
||||
if len(timeout_reset_states) >= 4 and not test2_complete.done():
|
||||
test2_complete.set_result(True)
|
||||
|
||||
# Test 3: TimeoutFilterConfigured - static value
|
||||
elif sensor_name == "timeout_static_sensor":
|
||||
timeout_static_states.append(state.state)
|
||||
# Expect 2 values: initial 55.5 + timeout fires with 99.9
|
||||
if len(timeout_static_states) >= 2 and not test3_complete.done():
|
||||
test3_complete.set_result(True)
|
||||
|
||||
# Test 4: TimeoutFilterConfigured - lambda
|
||||
elif sensor_name == "timeout_lambda_sensor":
|
||||
timeout_lambda_states.append(state.state)
|
||||
# Expect 2 values: initial 77.7 + timeout fires with -1.0
|
||||
if len(timeout_lambda_states) >= 2 and not test4_complete.done():
|
||||
test4_complete.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, services = await client.list_entities_services()
|
||||
|
||||
key_to_sensor = build_key_to_entity_mapping(
|
||||
entities,
|
||||
[
|
||||
"timeout_last_sensor",
|
||||
"timeout_reset_sensor",
|
||||
"timeout_static_sensor",
|
||||
"timeout_lambda_sensor",
|
||||
],
|
||||
)
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Helper to find buttons by object_id substring
|
||||
def find_button(object_id_substring: str) -> int:
|
||||
"""Find a button by object_id substring and return its key."""
|
||||
button = next(
|
||||
(e for e in entities if object_id_substring in e.object_id.lower()),
|
||||
None,
|
||||
)
|
||||
assert button is not None, f"Button '{object_id_substring}' not found"
|
||||
return button.key
|
||||
|
||||
# Find all test buttons
|
||||
test1_button_key = find_button("test_timeout_last_button")
|
||||
test2_button_key = find_button("test_timeout_reset_button")
|
||||
test3_button_key = find_button("test_timeout_static_button")
|
||||
test4_button_key = find_button("test_timeout_lambda_button")
|
||||
|
||||
# === Test 1: TimeoutFilter - last mode ===
|
||||
client.button_command(test1_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test1_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 1 timeout. Received states: {timeout_last_states}")
|
||||
|
||||
assert len(timeout_last_states) == 2, (
|
||||
f"Test 1: Should have 2 states, got {len(timeout_last_states)}: {timeout_last_states}"
|
||||
)
|
||||
assert timeout_last_states[0] == pytest.approx(42.0), (
|
||||
f"Test 1: First state should be 42.0, got {timeout_last_states[0]}"
|
||||
)
|
||||
assert timeout_last_states[1] == pytest.approx(42.0), (
|
||||
f"Test 1: Timeout should output last value (42.0), got {timeout_last_states[1]}"
|
||||
)
|
||||
|
||||
# === Test 2: TimeoutFilter - reset behavior ===
|
||||
client.button_command(test2_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test2_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 2 timeout. Received states: {timeout_reset_states}")
|
||||
|
||||
assert len(timeout_reset_states) == 4, (
|
||||
f"Test 2: Should have 4 states, got {len(timeout_reset_states)}: {timeout_reset_states}"
|
||||
)
|
||||
assert timeout_reset_states[0] == pytest.approx(10.0), (
|
||||
f"Test 2: First state should be 10.0, got {timeout_reset_states[0]}"
|
||||
)
|
||||
assert timeout_reset_states[1] == pytest.approx(20.0), (
|
||||
f"Test 2: Second state should be 20.0, got {timeout_reset_states[1]}"
|
||||
)
|
||||
assert timeout_reset_states[2] == pytest.approx(30.0), (
|
||||
f"Test 2: Third state should be 30.0, got {timeout_reset_states[2]}"
|
||||
)
|
||||
assert timeout_reset_states[3] == pytest.approx(30.0), (
|
||||
f"Test 2: Timeout should output last value (30.0), got {timeout_reset_states[3]}"
|
||||
)
|
||||
|
||||
# === Test 3: TimeoutFilterConfigured - static value ===
|
||||
client.button_command(test3_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test3_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 3 timeout. Received states: {timeout_static_states}")
|
||||
|
||||
assert len(timeout_static_states) == 2, (
|
||||
f"Test 3: Should have 2 states, got {len(timeout_static_states)}: {timeout_static_states}"
|
||||
)
|
||||
assert timeout_static_states[0] == pytest.approx(55.5), (
|
||||
f"Test 3: First state should be 55.5, got {timeout_static_states[0]}"
|
||||
)
|
||||
assert timeout_static_states[1] == pytest.approx(99.9), (
|
||||
f"Test 3: Timeout should output configured value (99.9), got {timeout_static_states[1]}"
|
||||
)
|
||||
|
||||
# === Test 4: TimeoutFilterConfigured - lambda ===
|
||||
client.button_command(test4_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test4_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 4 timeout. Received states: {timeout_lambda_states}")
|
||||
|
||||
assert len(timeout_lambda_states) == 2, (
|
||||
f"Test 4: Should have 2 states, got {len(timeout_lambda_states)}: {timeout_lambda_states}"
|
||||
)
|
||||
assert timeout_lambda_states[0] == pytest.approx(77.7), (
|
||||
f"Test 4: First state should be 77.7, got {timeout_lambda_states[0]}"
|
||||
)
|
||||
assert timeout_lambda_states[1] == pytest.approx(-1.0), (
|
||||
f"Test 4: Timeout should evaluate lambda (-1.0), got {timeout_lambda_states[1]}"
|
||||
)
|
||||
@@ -7,3 +7,27 @@ some_component:
|
||||
value: 2
|
||||
- id: component2
|
||||
value: 5
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page1
|
||||
widgets:
|
||||
- obj:
|
||||
id: object1
|
||||
x: 3
|
||||
y: 2
|
||||
width: 4
|
||||
- obj:
|
||||
id: object3
|
||||
x: 6
|
||||
y: 12
|
||||
widgets:
|
||||
- obj:
|
||||
id: object4
|
||||
x: 14
|
||||
y: 9
|
||||
width: 15
|
||||
height: 13
|
||||
- obj:
|
||||
id: object5
|
||||
x: 10
|
||||
y: 11
|
||||
|
||||
@@ -13,6 +13,30 @@ packages:
|
||||
value: 5
|
||||
- id: component3
|
||||
value: 6
|
||||
- lvgl:
|
||||
pages:
|
||||
- id: page1
|
||||
widgets:
|
||||
- obj:
|
||||
id: object1
|
||||
x: 1
|
||||
y: 2
|
||||
- obj:
|
||||
id: object2
|
||||
x: 5
|
||||
- obj:
|
||||
id: object3
|
||||
x: 6
|
||||
y: 7
|
||||
widgets:
|
||||
- obj:
|
||||
id: object4
|
||||
x: 8
|
||||
y: 9
|
||||
- obj:
|
||||
id: object5
|
||||
x: 10
|
||||
y: 11
|
||||
|
||||
some_component:
|
||||
- id: !extend ${A}
|
||||
@@ -20,3 +44,23 @@ some_component:
|
||||
- id: component2
|
||||
value: 3
|
||||
- id: !remove ${C}
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: !extend page1
|
||||
widgets:
|
||||
- obj:
|
||||
id: !extend object1
|
||||
x: 3
|
||||
width: 4
|
||||
- obj:
|
||||
id: !remove object2
|
||||
- obj:
|
||||
id: !extend object3
|
||||
y: 12
|
||||
height: 13
|
||||
widgets:
|
||||
- obj:
|
||||
id: !extend object4
|
||||
x: 14
|
||||
width: 15
|
||||
|
||||
Reference in New Issue
Block a user