mirror of
https://github.com/esphome/esphome.git
synced 2026-01-25 14:02:08 -07:00
Compare commits
62 Commits
2026.1.2
...
libretiny_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76b1201c96 | ||
|
|
680e92a226 | ||
|
|
db0b32bfc9 | ||
|
|
21794e28e5 | ||
|
|
728236270c | ||
|
|
01cdc4ed58 | ||
|
|
d6a0c8ffbb | ||
|
|
4cc0f874f7 | ||
|
|
ed58b9372f | ||
|
|
ee2a81923b | ||
|
|
0a1e7ee50b | ||
|
|
4d4283bcfa | ||
|
|
e4fb6988ff | ||
|
|
d31b733dce | ||
|
|
b25a2f8d8e | ||
|
|
3f892711c7 | ||
|
|
798d3bd956 | ||
|
|
d830787c71 | ||
|
|
1f4221abfa | ||
|
|
92808a09c7 | ||
|
|
e54d5ee898 | ||
|
|
bbe1155518 | ||
|
|
69d7b6e921 | ||
|
|
510c874061 | ||
|
|
f7ad324d81 | ||
|
|
58a9e30017 | ||
|
|
52ac9e1861 | ||
|
|
c5e4a60884 | ||
|
|
a680884138 | ||
|
|
6832efbacc | ||
|
|
3057a0484f | ||
|
|
bc78f80f77 | ||
|
|
916b028fb2 | ||
|
|
16adae7359 | ||
|
|
4906f87751 | ||
|
|
5b37d2fb27 | ||
|
|
68affe0b9c | ||
|
|
8263a8273f | ||
|
|
14b7539094 | ||
|
|
b37cb812a7 | ||
|
|
42491569c8 | ||
|
|
b1230ec6bb | ||
|
|
4eda9e965f | ||
|
|
d2528af649 | ||
|
|
2eabc1b96b | ||
|
|
535c3eb2a2 | ||
|
|
20f937692e | ||
|
|
00cc9e44b6 | ||
|
|
0427350101 | ||
|
|
41dceb76ec | ||
|
|
6380458d78 | ||
|
|
0dc5a7c9a4 | ||
|
|
9003844eda | ||
|
|
22a4ec69c2 | ||
|
|
9d42bfd161 | ||
|
|
49c881d067 | ||
|
|
78aee4f498 | ||
|
|
9da2c08f36 | ||
|
|
03f3deff41 | ||
|
|
f1e5d3a39a | ||
|
|
2f6863230d | ||
|
|
f44036310c |
96
.claude/skills/pr-workflow/SKILL.md
Normal file
96
.claude/skills/pr-workflow/SKILL.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: pr-workflow
|
||||
description: Create pull requests for esphome. Use when creating PRs, submitting changes, or preparing contributions.
|
||||
allowed-tools: Read, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# ESPHome PR Workflow
|
||||
|
||||
When creating a pull request for esphome, follow these steps:
|
||||
|
||||
## 1. Create Branch from Upstream
|
||||
|
||||
Always base your branch on **upstream** (not origin/fork) to ensure you have the latest code:
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
git checkout -b <branch-name> upstream/dev
|
||||
```
|
||||
|
||||
## 2. Read the PR Template
|
||||
|
||||
Before creating a PR, read `.github/PULL_REQUEST_TEMPLATE.md` to understand required fields.
|
||||
|
||||
## 3. Create the PR
|
||||
|
||||
Use `gh pr create` with the **full template** filled in. Never skip or abbreviate sections.
|
||||
|
||||
Required fields:
|
||||
- **What does this implement/fix?**: Brief description of changes
|
||||
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
|
||||
- **Related issue**: Use `fixes <link>` syntax if applicable
|
||||
- **Pull request in esphome-docs**: Link if docs are needed
|
||||
- **Test Environment**: Check platforms you tested on
|
||||
- **Example config.yaml**: Include working example YAML
|
||||
- **Checklist**: Verify code is tested and tests added
|
||||
|
||||
## 4. Example PR Body
|
||||
|
||||
```markdown
|
||||
# What does this implement/fix?
|
||||
|
||||
<describe your changes here>
|
||||
|
||||
## Types of changes
|
||||
|
||||
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||
- [x] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Developer breaking change (an API change that could break external components)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
- [ ] Other
|
||||
|
||||
**Related issue or feature (if applicable):**
|
||||
|
||||
- fixes https://github.com/esphome/esphome/issues/XXX
|
||||
|
||||
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
|
||||
|
||||
- esphome/esphome-docs#XXX
|
||||
|
||||
## Test Environment
|
||||
|
||||
- [x] ESP32
|
||||
- [x] ESP32 IDF
|
||||
- [ ] ESP8266
|
||||
- [ ] RP2040
|
||||
- [ ] BK72xx
|
||||
- [ ] RTL87xx
|
||||
- [ ] LN882x
|
||||
- [ ] nRF52840
|
||||
|
||||
## Example entry for `config.yaml`:
|
||||
|
||||
```yaml
|
||||
# Example config.yaml
|
||||
component_name:
|
||||
id: my_component
|
||||
option: value
|
||||
```
|
||||
|
||||
## Checklist:
|
||||
- [x] The code change is tested and works locally.
|
||||
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
|
||||
```
|
||||
|
||||
## 5. Push and Create PR
|
||||
|
||||
```bash
|
||||
git push -u origin <branch-name>
|
||||
gh pr create --repo esphome/esphome --base dev --title "[component] Brief description"
|
||||
```
|
||||
|
||||
Title should be prefixed with the component name in brackets, e.g. `[safe_mode] Add feature`.
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -223,7 +223,7 @@ jobs:
|
||||
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -334,14 +334,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -413,14 +413,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -502,14 +502,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -735,7 +735,7 @@ jobs:
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -759,7 +759,7 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -800,7 +800,7 @@ jobs:
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -847,7 +847,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.11
|
||||
rev: v0.14.13
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.1.2
|
||||
PROJECT_NUMBER = 2026.2.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
import argparse
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import getpass
|
||||
@@ -223,8 +222,13 @@ def choose_upload_log_host(
|
||||
else:
|
||||
resolved.append(device)
|
||||
if not resolved:
|
||||
if CORE.dashboard:
|
||||
hint = "If you know the IP, set 'use_address' in your network config."
|
||||
else:
|
||||
hint = "If you know the IP, try --device <IP>"
|
||||
raise EsphomeError(
|
||||
f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
|
||||
f"All specified devices {defaults} could not be resolved. "
|
||||
f"Is the device connected to the network? {hint}"
|
||||
)
|
||||
return resolved
|
||||
|
||||
@@ -932,21 +936,11 @@ def command_dashboard(args: ArgsProtocol) -> int | None:
|
||||
return dashboard.start_dashboard(args)
|
||||
|
||||
|
||||
def run_multiple_configs(
|
||||
files: list, command_builder: Callable[[str], list[str]]
|
||||
) -> int:
|
||||
"""Run a command for each configuration file in a subprocess.
|
||||
|
||||
Args:
|
||||
files: List of configuration files to process.
|
||||
command_builder: Callable that takes a file path and returns a command list.
|
||||
|
||||
Returns:
|
||||
Number of failed files.
|
||||
"""
|
||||
def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
import click
|
||||
|
||||
success = {}
|
||||
files = list_yaml_files(args.configuration)
|
||||
twidth = 60
|
||||
|
||||
def print_bar(middle_text):
|
||||
@@ -956,19 +950,17 @@ def run_multiple_configs(
|
||||
safe_print(f"{half_line}{middle_text}{half_line}")
|
||||
|
||||
for f in files:
|
||||
f_path = Path(f) if not isinstance(f, Path) else f
|
||||
|
||||
if any(f_path.name == x for x in SECRETS_FILES):
|
||||
_LOGGER.warning("Skipping secrets file %s", f_path)
|
||||
continue
|
||||
|
||||
safe_print(f"Processing {color(AnsiFore.CYAN, str(f))}")
|
||||
safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
|
||||
safe_print("-" * twidth)
|
||||
safe_print()
|
||||
|
||||
cmd = command_builder(f)
|
||||
rc = run_external_process(*cmd)
|
||||
|
||||
if CORE.dashboard:
|
||||
rc = run_external_process(
|
||||
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
|
||||
)
|
||||
else:
|
||||
rc = run_external_process(
|
||||
"esphome", "run", f, "--no-logs", "--device", "OTA"
|
||||
)
|
||||
if rc == 0:
|
||||
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
|
||||
success[f] = True
|
||||
@@ -983,8 +975,6 @@ def run_multiple_configs(
|
||||
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
|
||||
failed = 0
|
||||
for f in files:
|
||||
if f not in success:
|
||||
continue # Skipped file
|
||||
if success[f]:
|
||||
safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
|
||||
else:
|
||||
@@ -993,17 +983,6 @@ def run_multiple_configs(
|
||||
return failed
|
||||
|
||||
|
||||
def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
files = list_yaml_files(args.configuration)
|
||||
|
||||
def build_command(f):
|
||||
if CORE.dashboard:
|
||||
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
|
||||
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
|
||||
|
||||
return run_multiple_configs(files, build_command)
|
||||
|
||||
|
||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
import json
|
||||
|
||||
@@ -1554,48 +1533,38 @@ def run_esphome(argv):
|
||||
|
||||
_LOGGER.info("ESPHome %s", const.__version__)
|
||||
|
||||
# Multiple configurations: use subprocesses to avoid state leakage
|
||||
# between compilations (e.g., LVGL touchscreen state in module globals)
|
||||
if len(args.configuration) > 1:
|
||||
# Build command by reusing argv, replacing all configs with single file
|
||||
# argv[0] is the program path, skip it since we prefix with "esphome"
|
||||
def build_command(f):
|
||||
return (
|
||||
["esphome"]
|
||||
+ [arg for arg in argv[1:] if arg not in args.configuration]
|
||||
+ [str(f)]
|
||||
)
|
||||
for conf_path in args.configuration:
|
||||
conf_path = Path(conf_path)
|
||||
if any(conf_path.name == x for x in SECRETS_FILES):
|
||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||
continue
|
||||
|
||||
return run_multiple_configs(args.configuration, build_command)
|
||||
CORE.config_path = conf_path
|
||||
CORE.dashboard = args.dashboard
|
||||
|
||||
# Single configuration
|
||||
conf_path = Path(args.configuration[0])
|
||||
if any(conf_path.name == x for x in SECRETS_FILES):
|
||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||
return 0
|
||||
# For logs command, skip updating external components
|
||||
skip_external = args.command == "logs"
|
||||
config = read_config(
|
||||
dict(args.substitution) if args.substitution else {},
|
||||
skip_external_update=skip_external,
|
||||
)
|
||||
if config is None:
|
||||
return 2
|
||||
CORE.config = config
|
||||
|
||||
CORE.config_path = conf_path
|
||||
CORE.dashboard = args.dashboard
|
||||
if args.command not in POST_CONFIG_ACTIONS:
|
||||
safe_print(f"Unknown command {args.command}")
|
||||
|
||||
# For logs command, skip updating external components
|
||||
skip_external = args.command == "logs"
|
||||
config = read_config(
|
||||
dict(args.substitution) if args.substitution else {},
|
||||
skip_external_update=skip_external,
|
||||
)
|
||||
if config is None:
|
||||
return 2
|
||||
CORE.config = config
|
||||
try:
|
||||
rc = POST_CONFIG_ACTIONS[args.command](args, config)
|
||||
except EsphomeError as e:
|
||||
_LOGGER.error(e, exc_info=args.verbose)
|
||||
return 1
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
if args.command not in POST_CONFIG_ACTIONS:
|
||||
safe_print(f"Unknown command {args.command}")
|
||||
return 1
|
||||
|
||||
try:
|
||||
return POST_CONFIG_ACTIONS[args.command](args, config)
|
||||
except EsphomeError as e:
|
||||
_LOGGER.error(e, exc_info=args.verbose)
|
||||
return 1
|
||||
CORE.reset()
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -22,7 +22,7 @@ from .helpers import (
|
||||
map_section_name,
|
||||
parse_symbol_line,
|
||||
)
|
||||
from .toolchain import find_tool, run_tool
|
||||
from .toolchain import find_tool, resolve_tool_path, run_tool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.platformio_api import IDEData
|
||||
@@ -132,6 +132,12 @@ class MemoryAnalyzer:
|
||||
readelf_path = readelf_path or idedata.readelf_path
|
||||
_LOGGER.debug("Using toolchain paths from PlatformIO idedata")
|
||||
|
||||
# Validate paths exist, fall back to find_tool if they don't
|
||||
# This handles cases like Zephyr where cc_path doesn't include full path
|
||||
# and the toolchain prefix may differ (e.g., arm-zephyr-eabi- vs arm-none-eabi-)
|
||||
objdump_path = resolve_tool_path("objdump", objdump_path, objdump_path)
|
||||
readelf_path = resolve_tool_path("readelf", readelf_path, objdump_path)
|
||||
|
||||
self.objdump_path = objdump_path or "objdump"
|
||||
self.readelf_path = readelf_path or "readelf"
|
||||
self.external_components = external_components or set()
|
||||
|
||||
@@ -9,11 +9,61 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
|
||||
# Maps standard section names to their various platform-specific variants
|
||||
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
|
||||
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
|
||||
#
|
||||
# Platform-specific sections:
|
||||
# - ESP8266/ESP32: .iram*, .dram*
|
||||
# - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM)
|
||||
# - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors)
|
||||
# - LibreTiny LN882X: .flash_text, .flash_copy* (flash code)
|
||||
# - Zephyr/nRF52: text, rodata, datas, bss (no leading dots)
|
||||
SECTION_MAPPING = {
|
||||
".text": frozenset([".text", ".iram"]),
|
||||
".rodata": frozenset([".rodata"]),
|
||||
".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss"
|
||||
".data": frozenset([".data", ".dram"]),
|
||||
".text": frozenset(
|
||||
[
|
||||
".text",
|
||||
".iram",
|
||||
# LibreTiny RTL87xx XIP (eXecute In Place) flash code
|
||||
".xip.code",
|
||||
# LibreTiny RTL87xx RAM code
|
||||
".ram.code_text",
|
||||
# LibreTiny BK7231 fast RAM code and vectors
|
||||
".itcm.code",
|
||||
".vectors",
|
||||
# LibreTiny LN882X flash code
|
||||
".flash_text",
|
||||
".flash_copy",
|
||||
# Zephyr/nRF52 sections (no leading dots)
|
||||
"text",
|
||||
"rom_start",
|
||||
]
|
||||
),
|
||||
".rodata": frozenset(
|
||||
[
|
||||
".rodata",
|
||||
# LibreTiny RTL87xx read-only data in RAM
|
||||
".ram.code_rodata",
|
||||
# Zephyr/nRF52 sections (no leading dots)
|
||||
"rodata",
|
||||
]
|
||||
),
|
||||
# .bss patterns - must be before .data to catch ".dram0.bss"
|
||||
".bss": frozenset(
|
||||
[
|
||||
".bss",
|
||||
# LibreTiny LN882X BSS
|
||||
".bss_ram",
|
||||
# Zephyr/nRF52 sections (no leading dots)
|
||||
"bss",
|
||||
"noinit",
|
||||
]
|
||||
),
|
||||
".data": frozenset(
|
||||
[
|
||||
".data",
|
||||
".dram",
|
||||
# Zephyr/nRF52 sections (no leading dots)
|
||||
"datas",
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
# Section to ComponentMemory attribute mapping
|
||||
|
||||
@@ -94,13 +94,13 @@ def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
|
||||
return None
|
||||
|
||||
# Find section, size, and name
|
||||
# Try each part as a potential section name
|
||||
for i, part in enumerate(parts):
|
||||
if not part.startswith("."):
|
||||
continue
|
||||
|
||||
# Skip parts that are clearly flags, addresses, or other metadata
|
||||
# Sections start with '.' (standard ELF) or are known section names (Zephyr)
|
||||
section = map_section_name(part)
|
||||
if not section:
|
||||
break
|
||||
continue
|
||||
|
||||
# Need at least size field after section
|
||||
if i + 1 >= len(parts):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -17,10 +18,82 @@ TOOLCHAIN_PREFIXES = [
|
||||
"xtensa-lx106-elf-", # ESP8266
|
||||
"xtensa-esp32-elf-", # ESP32
|
||||
"xtensa-esp-elf-", # ESP32 (newer IDF)
|
||||
"arm-zephyr-eabi-", # nRF52/Zephyr SDK
|
||||
"arm-none-eabi-", # Generic ARM (RP2040, etc.)
|
||||
"", # System default (no prefix)
|
||||
]
|
||||
|
||||
|
||||
def _find_in_platformio_packages(tool_name: str) -> str | None:
|
||||
"""Search for a tool in PlatformIO package directories.
|
||||
|
||||
This handles cases like Zephyr SDK where tools are installed in nested
|
||||
directories that aren't in PATH.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool (e.g., "readelf", "objdump")
|
||||
|
||||
Returns:
|
||||
Full path to the tool or None if not found
|
||||
"""
|
||||
# Get PlatformIO packages directory
|
||||
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
|
||||
if not platformio_home.exists():
|
||||
return None
|
||||
|
||||
# Search patterns for toolchains that might contain the tool
|
||||
# Order matters - more specific patterns first
|
||||
search_patterns = [
|
||||
# Zephyr SDK deeply nested structure (4 levels)
|
||||
# e.g., toolchain-gccarmnoneeabi/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump
|
||||
f"toolchain-*/*/*/bin/*-{tool_name}",
|
||||
# Zephyr SDK nested structure (3 levels)
|
||||
f"toolchain-*/*/bin/*-{tool_name}",
|
||||
f"toolchain-*/bin/*-{tool_name}",
|
||||
# Standard PlatformIO toolchain structure
|
||||
f"toolchain-*/bin/*{tool_name}",
|
||||
]
|
||||
|
||||
for pattern in search_patterns:
|
||||
matches = list(platformio_home.glob(pattern))
|
||||
if matches:
|
||||
# Sort to get consistent results, prefer arm-zephyr-eabi over arm-none-eabi
|
||||
matches.sort(key=lambda p: ("zephyr" not in str(p), str(p)))
|
||||
tool_path = str(matches[0])
|
||||
_LOGGER.debug("Found %s in PlatformIO packages: %s", tool_name, tool_path)
|
||||
return tool_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_tool_path(
|
||||
tool_name: str,
|
||||
derived_path: str | None,
|
||||
objdump_path: str | None = None,
|
||||
) -> str | None:
|
||||
"""Resolve a tool path, falling back to find_tool if derived path doesn't exist.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool (e.g., "objdump", "readelf")
|
||||
derived_path: Path derived from idedata (may not exist for some platforms)
|
||||
objdump_path: Path to objdump binary to derive other tool paths from
|
||||
|
||||
Returns:
|
||||
Resolved path to the tool, or the original derived_path if it exists
|
||||
"""
|
||||
if derived_path and not Path(derived_path).exists():
|
||||
found = find_tool(tool_name, objdump_path)
|
||||
if found:
|
||||
_LOGGER.debug(
|
||||
"Derived %s path %s not found, using %s",
|
||||
tool_name,
|
||||
derived_path,
|
||||
found,
|
||||
)
|
||||
return found
|
||||
return derived_path
|
||||
|
||||
|
||||
def find_tool(
|
||||
tool_name: str,
|
||||
objdump_path: str | None = None,
|
||||
@@ -28,7 +101,8 @@ def find_tool(
|
||||
"""Find a toolchain tool by name.
|
||||
|
||||
First tries to derive the tool path from objdump_path (if provided),
|
||||
then falls back to searching for platform-specific tools.
|
||||
then searches PlatformIO package directories (for cross-compile toolchains),
|
||||
and finally falls back to searching for platform-specific tools in PATH.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
|
||||
@@ -47,7 +121,13 @@ def find_tool(
|
||||
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
|
||||
return potential_path
|
||||
|
||||
# Try platform-specific tools
|
||||
# Search in PlatformIO packages directory first (handles Zephyr SDK, etc.)
|
||||
# This must come before PATH search because system tools (e.g., /usr/bin/objdump)
|
||||
# are for the host architecture, not the target (ARM, Xtensa, etc.)
|
||||
if found := _find_in_platformio_packages(tool_name):
|
||||
return found
|
||||
|
||||
# Try platform-specific tools in PATH (fallback for when tools are installed globally)
|
||||
for prefix in TOOLCHAIN_PREFIXES:
|
||||
cmd = f"{prefix}{tool_name}"
|
||||
try:
|
||||
|
||||
@@ -1712,16 +1712,17 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
}
|
||||
|
||||
// Create null-terminated state for callback (parse_number needs null-termination)
|
||||
// HA state max length is 255 characters, but attributes can be much longer
|
||||
// Use stack buffer for common case (states), heap fallback for large attributes
|
||||
size_t state_len = msg.state.size();
|
||||
SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1);
|
||||
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
|
||||
if (state_len > 0) {
|
||||
memcpy(state_buf, msg.state.c_str(), state_len);
|
||||
// HA state max length is 255, so 256 byte buffer covers all cases
|
||||
char state_buf[256];
|
||||
size_t copy_len = msg.state.size();
|
||||
if (copy_len >= sizeof(state_buf)) {
|
||||
copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
|
||||
}
|
||||
state_buf[state_len] = '\0';
|
||||
it.callback(StringRef(state_buf, state_len));
|
||||
if (copy_len > 0) {
|
||||
memcpy(state_buf, msg.state.c_str(), copy_len);
|
||||
}
|
||||
state_buf[copy_len] = '\0';
|
||||
it.callback(StringRef(state_buf, copy_len));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1844,8 +1845,23 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
|
||||
this->helper_->set_nodelay_for_message(is_log_message);
|
||||
// Toggle Nagle's algorithm based on message type to prevent log messages from
|
||||
// filling the TCP send buffer and crowding out important state updates.
|
||||
//
|
||||
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
|
||||
// message with `option (no_delay) = false;` in api.proto, indicating it should
|
||||
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
|
||||
//
|
||||
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
|
||||
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
|
||||
// (~200ms), buffer filling, or when a state update triggers a flush.
|
||||
//
|
||||
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
|
||||
// for immediate delivery. These are time-sensitive and should not be delayed.
|
||||
//
|
||||
// This must be done proactively BEFORE the buffer fills up - checking buffer
|
||||
// state here would be too late since we'd already be in a degraded state.
|
||||
this->helper_->set_nodelay(!is_log_message);
|
||||
|
||||
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
|
||||
if (err == APIError::WOULD_BLOCK)
|
||||
|
||||
@@ -120,39 +120,26 @@ class APIFrameHelper {
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
// Manage TCP_NODELAY (Nagle's algorithm) based on message type.
|
||||
//
|
||||
// For non-log messages (sensor data, state updates): Always disable Nagle
|
||||
// (NODELAY on) for immediate delivery - these are time-sensitive.
|
||||
//
|
||||
// For log messages: Use Nagle to coalesce multiple small log packets into
|
||||
// fewer larger packets, reducing WiFi overhead. However, we limit batching
|
||||
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
|
||||
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
|
||||
// shared pbufs, but holding data too long waiting for Nagle's timer causes
|
||||
// buffer exhaustion and dropped messages.
|
||||
//
|
||||
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
|
||||
//
|
||||
void set_nodelay_for_message(bool is_log_message) {
|
||||
if (!is_log_message) {
|
||||
if (this->nodelay_state_ != NODELAY_ON) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
|
||||
if (this->nodelay_state_ == NODELAY_ON) {
|
||||
this->set_nodelay_raw_(false);
|
||||
this->nodelay_state_ = 1;
|
||||
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
} else {
|
||||
this->nodelay_state_++;
|
||||
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
|
||||
///
|
||||
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
|
||||
/// state updates low-latency (NODELAY enabled). Without this, many small log
|
||||
/// packets fill the TCP send buffer, crowding out important state updates.
|
||||
///
|
||||
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
|
||||
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
|
||||
///
|
||||
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
|
||||
/// @return true if successful or already in desired state
|
||||
bool set_nodelay(bool enable) {
|
||||
if (this->nodelay_enabled_ == enable)
|
||||
return true;
|
||||
int val = enable ? 1 : 0;
|
||||
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
if (err == 0) {
|
||||
this->nodelay_enabled_ = enable;
|
||||
}
|
||||
return err == 0;
|
||||
}
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf messages in a single operation
|
||||
@@ -242,18 +229,10 @@ class APIFrameHelper {
|
||||
uint8_t tx_buf_head_{0};
|
||||
uint8_t tx_buf_tail_{0};
|
||||
uint8_t tx_buf_count_{0};
|
||||
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
|
||||
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
|
||||
static constexpr int8_t NODELAY_ON = -1;
|
||||
static constexpr int8_t LOG_NAGLE_COUNT = 2;
|
||||
int8_t nodelay_state_{NODELAY_ON};
|
||||
|
||||
// Internal helper to set TCP_NODELAY socket option
|
||||
void set_nodelay_raw_(bool enable) {
|
||||
int val = enable ? 1 : 0;
|
||||
this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
}
|
||||
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
|
||||
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
|
||||
// messages to coalesce while keeping state updates low-latency.
|
||||
bool nodelay_enabled_{true};
|
||||
|
||||
// Common initialization for both plaintext and noise protocols
|
||||
APIError init_common_();
|
||||
|
||||
@@ -13,11 +13,14 @@ from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
|
||||
CODEOWNERS = ["@jasstrong"]
|
||||
DEPENDENCIES = ["sensor"]
|
||||
|
||||
UNIT_INDEX = "index"
|
||||
|
||||
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
AQISensor,
|
||||
unit_of_measurement=UNIT_INDEX,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
|
||||
@@ -158,12 +158,14 @@ void ATM90E32Component::setup() {
|
||||
|
||||
if (this->enable_offset_calibration_) {
|
||||
// Initialize flash storage for offset calibrations
|
||||
uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_);
|
||||
uint32_t o_hash = fnv1_hash("_offset_calibration_");
|
||||
o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
|
||||
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
|
||||
this->restore_offset_calibrations_();
|
||||
|
||||
// Initialize flash storage for power offset calibrations
|
||||
uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_);
|
||||
uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
|
||||
po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
|
||||
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
|
||||
this->restore_power_offset_calibrations_();
|
||||
} else {
|
||||
@@ -183,7 +185,8 @@ void ATM90E32Component::setup() {
|
||||
|
||||
if (this->enable_gain_calibration_) {
|
||||
// Initialize flash storage for gain calibration
|
||||
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_);
|
||||
uint32_t g_hash = fnv1_hash("_gain_calibration_");
|
||||
g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
|
||||
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
|
||||
this->restore_gain_calibrations_();
|
||||
|
||||
|
||||
@@ -185,18 +185,16 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
|
||||
return err;
|
||||
}
|
||||
|
||||
std::string url_string = str_lower_case(url);
|
||||
|
||||
if (str_endswith(url_string, ".wav")) {
|
||||
if (str_endswith_ignore_case(url, ".wav")) {
|
||||
file_type = AudioFileType::WAV;
|
||||
}
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
else if (str_endswith(url_string, ".mp3")) {
|
||||
else if (str_endswith_ignore_case(url, ".mp3")) {
|
||||
file_type = AudioFileType::MP3;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
else if (str_endswith(url_string, ".flac")) {
|
||||
else if (str_endswith_ignore_case(url, ".flac")) {
|
||||
file_type = AudioFileType::FLAC;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -74,8 +74,11 @@ class DebugComponent : public PollingComponent {
|
||||
#ifdef USE_SENSOR
|
||||
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
|
||||
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
|
||||
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
|
||||
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
void set_min_free_sensor(sensor::Sensor *min_free_sensor) { min_free_sensor_ = min_free_sensor; }
|
||||
#endif
|
||||
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
|
||||
#ifdef USE_ESP32
|
||||
@@ -97,8 +100,11 @@ class DebugComponent : public PollingComponent {
|
||||
|
||||
sensor::Sensor *free_sensor_{nullptr};
|
||||
sensor::Sensor *block_sensor_{nullptr};
|
||||
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
|
||||
sensor::Sensor *fragmentation_sensor_{nullptr};
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
sensor::Sensor *min_free_sensor_{nullptr};
|
||||
#endif
|
||||
sensor::Sensor *loop_time_sensor_{nullptr};
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -234,8 +234,19 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
|
||||
void DebugComponent::update_platform_() {
|
||||
#ifdef USE_SENSOR
|
||||
uint32_t max_alloc = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
|
||||
if (this->block_sensor_ != nullptr) {
|
||||
this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
|
||||
this->block_sensor_->publish_state(max_alloc);
|
||||
}
|
||||
if (this->min_free_sensor_ != nullptr) {
|
||||
this->min_free_sensor_->publish_state(heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL));
|
||||
}
|
||||
if (this->fragmentation_sensor_ != nullptr) {
|
||||
uint32_t free_heap = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
if (free_heap > 0) {
|
||||
float fragmentation = 100.0f - (100.0f * max_alloc / free_heap);
|
||||
this->fragmentation_sensor_->publish_state(fragmentation);
|
||||
}
|
||||
}
|
||||
if (this->psram_sensor_ != nullptr) {
|
||||
this->psram_sensor_->publish_state(heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
|
||||
|
||||
@@ -51,6 +51,9 @@ void DebugComponent::update_platform_() {
|
||||
if (this->block_sensor_ != nullptr) {
|
||||
this->block_sensor_->publish_state(lt_heap_get_max_alloc());
|
||||
}
|
||||
if (this->min_free_sensor_ != nullptr) {
|
||||
this->min_free_sensor_->publish_state(lt_heap_get_min_free());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ from esphome.const import (
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_COUNTER,
|
||||
ICON_TIMER,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
UNIT_BYTES,
|
||||
UNIT_HERTZ,
|
||||
UNIT_MILLISECOND,
|
||||
@@ -25,6 +28,7 @@ from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
|
||||
DEPENDENCIES = ["debug"]
|
||||
|
||||
CONF_MIN_FREE = "min_free"
|
||||
CONF_PSRAM = "psram"
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
@@ -42,8 +46,14 @@ CONFIG_SCHEMA = {
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_FRAGMENTATION): cv.All(
|
||||
cv.only_on_esp8266,
|
||||
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
|
||||
cv.Any(
|
||||
cv.All(
|
||||
cv.only_on_esp8266,
|
||||
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
|
||||
),
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_COUNTER,
|
||||
@@ -51,6 +61,19 @@ CONFIG_SCHEMA = {
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_MIN_FREE): cv.All(
|
||||
cv.Any(
|
||||
cv.only_on_esp32,
|
||||
cv.only_on([PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]),
|
||||
msg="This feature is only available on ESP32 and LibreTiny (BK72xx, LN882x, RTL87xx)",
|
||||
),
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BYTES,
|
||||
icon=ICON_COUNTER,
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MILLISECOND,
|
||||
icon=ICON_TIMER,
|
||||
@@ -93,6 +116,10 @@ async def to_code(config):
|
||||
sens = await sensor.new_sensor(fragmentation_conf)
|
||||
cg.add(debug_component.set_fragmentation_sensor(sens))
|
||||
|
||||
if min_free_conf := config.get(CONF_MIN_FREE):
|
||||
sens = await sensor.new_sensor(min_free_conf)
|
||||
cg.add(debug_component.set_min_free_sensor(sens))
|
||||
|
||||
if loop_time_conf := config.get(CONF_LOOP_TIME):
|
||||
sens = await sensor.new_sensor(loop_time_conf)
|
||||
cg.add(debug_component.set_loop_time_sensor(sens))
|
||||
|
||||
@@ -89,8 +89,10 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
delayMicroseconds(500);
|
||||
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
|
||||
delayMicroseconds(2000);
|
||||
} else {
|
||||
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
|
||||
delayMicroseconds(1000);
|
||||
} else {
|
||||
delayMicroseconds(800);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -190,7 +190,7 @@ async def to_code(config):
|
||||
# Rotation is handled by setting the transform
|
||||
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
|
||||
await display.register_display(var, display_config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -180,12 +180,6 @@ def set_core_data(config):
|
||||
path=[CONF_CPU_FREQUENCY],
|
||||
)
|
||||
|
||||
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
|
||||
_LOGGER.warning(
|
||||
"400MHz on ESP32-P4 is experimental and may not boot. "
|
||||
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
|
||||
)
|
||||
|
||||
CORE.data[KEY_ESP32] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
|
||||
@@ -12,7 +12,6 @@ from esphome.const import (
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import add_define
|
||||
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
|
||||
@@ -43,7 +42,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
add_define("USE_ESP32_HOSTED")
|
||||
if config[CONF_ACTIVE_HIGH]:
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#endif
|
||||
@@ -182,23 +181,15 @@ bool Esp32HostedUpdate::fetch_manifest_() {
|
||||
}
|
||||
|
||||
// Read manifest JSON into string (manifest is small, ~1KB max)
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
std::string json_str;
|
||||
json_str.reserve(container->content_length);
|
||||
uint8_t buf[256];
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read_or_error = container->read(buf, sizeof(buf));
|
||||
App.feed_wdt();
|
||||
int read = container->read(buf, sizeof(buf));
|
||||
if (read > 0) {
|
||||
json_str.append(reinterpret_cast<char *>(buf), read);
|
||||
}
|
||||
yield();
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != http_request::HttpReadLoopResult::DATA)
|
||||
break; // ERROR or TIMEOUT
|
||||
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
|
||||
}
|
||||
container->end();
|
||||
|
||||
@@ -303,38 +294,32 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
|
||||
}
|
||||
|
||||
// Stream firmware to coprocessor while computing SHA256
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
sha256::SHA256 hasher;
|
||||
hasher.init();
|
||||
|
||||
uint8_t buffer[CHUNK_SIZE];
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
|
||||
while (container->get_bytes_read() < total_size) {
|
||||
int read_or_error = container->read(buffer, sizeof(buffer));
|
||||
int read = container->read(buffer, sizeof(buffer));
|
||||
|
||||
// Feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != http_request::HttpReadLoopResult::DATA) {
|
||||
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading firmware data");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (read <= 0) {
|
||||
if (read < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Download failed"));
|
||||
return false;
|
||||
}
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Download failed"));
|
||||
return false;
|
||||
// read == 0: no more data available, exit loop
|
||||
break;
|
||||
}
|
||||
|
||||
hasher.add(buffer, read_or_error);
|
||||
err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
|
||||
hasher.add(buffer, read);
|
||||
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "fingerprint_grow.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -533,21 +532,14 @@ void FingerprintGrowComponent::sensor_sleep_() {
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::dump_config() {
|
||||
char sensing_pin_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char power_pin_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
if (this->has_sensing_pin_) {
|
||||
this->sensing_pin_->dump_summary(sensing_pin_buf, sizeof(sensing_pin_buf));
|
||||
}
|
||||
if (this->has_power_pin_) {
|
||||
this->sensor_power_pin_->dump_summary(power_pin_buf, sizeof(power_pin_buf));
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"GROW_FINGERPRINT_READER:\n"
|
||||
" System Identifier Code: 0x%.4X\n"
|
||||
" Touch Sensing Pin: %s\n"
|
||||
" Sensor Power Pin: %s",
|
||||
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
|
||||
this->has_power_pin_ ? power_pin_buf : "None");
|
||||
this->system_identifier_code_,
|
||||
this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None",
|
||||
this->has_power_pin_ ? this->sensor_power_pin_->dump_summary().c_str() : "None");
|
||||
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
|
||||
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
|
||||
} else {
|
||||
|
||||
@@ -157,7 +157,6 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
|
||||
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
|
||||
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
|
||||
|
||||
if config.get(CONF_VERIFY_SSL):
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
|
||||
@@ -79,81 +79,6 @@ inline bool is_redirect(int const status) {
|
||||
*/
|
||||
inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
|
||||
|
||||
/*
|
||||
* HTTP Container Read Semantics
|
||||
* =============================
|
||||
*
|
||||
* IMPORTANT: These semantics differ from standard BSD sockets!
|
||||
*
|
||||
* BSD socket read() returns:
|
||||
* > 0: bytes read
|
||||
* == 0: connection closed (EOF)
|
||||
* < 0: error (check errno)
|
||||
*
|
||||
* HttpContainer::read() returns:
|
||||
* > 0: bytes read successfully
|
||||
* == 0: no data available yet OR all content read
|
||||
* (caller should check bytes_read vs content_length)
|
||||
* < 0: error or connection closed (caller should EXIT)
|
||||
* HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
|
||||
* other negative values = platform-specific errors
|
||||
*
|
||||
* Platform behaviors:
|
||||
* - ESP-IDF: blocking reads, 0 only returned when all content read
|
||||
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
|
||||
*
|
||||
* Use the helper functions below instead of checking return values directly:
|
||||
* - http_read_loop_result(): for manual loops with per-chunk processing
|
||||
* - http_read_fully(): for simple "read N bytes into buffer" operations
|
||||
*/
|
||||
|
||||
/// Error code returned by HttpContainer::read() when connection closed prematurely
|
||||
/// NOTE: Unlike BSD sockets where 0 means EOF, here 0 means "no data yet, retry"
|
||||
static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1;
|
||||
|
||||
/// Status of a read operation
|
||||
enum class HttpReadStatus : uint8_t {
|
||||
OK, ///< Read completed successfully
|
||||
ERROR, ///< Read error occurred
|
||||
TIMEOUT, ///< Timeout waiting for data
|
||||
};
|
||||
|
||||
/// Result of an HTTP read operation
|
||||
struct HttpReadResult {
|
||||
HttpReadStatus status; ///< Status of the read operation
|
||||
int error_code; ///< Error code from read() on failure, 0 on success
|
||||
};
|
||||
|
||||
/// Result of processing a non-blocking read with timeout (for manual loops)
|
||||
enum class HttpReadLoopResult : uint8_t {
|
||||
DATA, ///< Data was read, process it
|
||||
RETRY, ///< No data yet, already delayed, caller should continue loop
|
||||
ERROR, ///< Read error, caller should exit loop
|
||||
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
||||
};
|
||||
|
||||
/// Process a read result with timeout tracking and delay handling
|
||||
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
|
||||
/// @param last_data_time Time of last successful read, updated when data received
|
||||
/// @param timeout_ms Maximum time to wait for data
|
||||
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
|
||||
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
|
||||
uint32_t timeout_ms) {
|
||||
if (bytes_read_or_error > 0) {
|
||||
last_data_time = millis();
|
||||
return HttpReadLoopResult::DATA;
|
||||
}
|
||||
if (bytes_read_or_error < 0) {
|
||||
return HttpReadLoopResult::ERROR;
|
||||
}
|
||||
// bytes_read_or_error == 0: no data available yet
|
||||
if (millis() - last_data_time >= timeout_ms) {
|
||||
return HttpReadLoopResult::TIMEOUT;
|
||||
}
|
||||
delay(1); // Small delay to prevent tight spinning
|
||||
return HttpReadLoopResult::RETRY;
|
||||
}
|
||||
|
||||
class HttpRequestComponent;
|
||||
|
||||
class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
@@ -163,33 +88,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
int status_code;
|
||||
uint32_t duration_ms;
|
||||
|
||||
/**
|
||||
* @brief Read data from the HTTP response body.
|
||||
*
|
||||
* WARNING: These semantics differ from BSD sockets!
|
||||
* BSD sockets: 0 = EOF (connection closed)
|
||||
* This method: 0 = no data yet OR all content read, negative = error/closed
|
||||
*
|
||||
* @param buf Buffer to read data into
|
||||
* @param max_len Maximum number of bytes to read
|
||||
* @return
|
||||
* - > 0: Number of bytes read successfully
|
||||
* - 0: No data available yet OR all content read
|
||||
* (check get_bytes_read() >= content_length to distinguish)
|
||||
* - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely
|
||||
* - < -1: Other error (platform-specific error code)
|
||||
*
|
||||
* Platform notes:
|
||||
* - ESP-IDF: blocking read, 0 only when all content read
|
||||
* - Arduino: non-blocking, 0 can mean "no data yet" or "all content read"
|
||||
*
|
||||
* Use get_bytes_read() and content_length to track progress.
|
||||
* When get_bytes_read() >= content_length, all data has been received.
|
||||
*
|
||||
* IMPORTANT: Do not use raw return values directly. Use these helpers:
|
||||
* - http_read_loop_result(): for loops with per-chunk processing
|
||||
* - http_read_fully(): for simple "read N bytes" operations
|
||||
*/
|
||||
virtual int read(uint8_t *buf, size_t max_len) = 0;
|
||||
virtual void end() = 0;
|
||||
|
||||
@@ -212,38 +110,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
std::map<std::string, std::list<std::string>> response_headers_{};
|
||||
};
|
||||
|
||||
/// Read data from HTTP container into buffer with timeout handling
|
||||
/// Handles feed_wdt, yield, and timeout checking internally
|
||||
/// @param container The HTTP container to read from
|
||||
/// @param buffer Buffer to read into
|
||||
/// @param total_size Total bytes to read
|
||||
/// @param chunk_size Maximum bytes per read call
|
||||
/// @param timeout_ms Read timeout in milliseconds
|
||||
/// @return HttpReadResult with status and error_code on failure
|
||||
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
|
||||
uint32_t timeout_ms) {
|
||||
size_t read_index = 0;
|
||||
uint32_t last_data_time = millis();
|
||||
|
||||
while (read_index < total_size) {
|
||||
int read_bytes_or_error = container->read(buffer + read_index, std::min(chunk_size, total_size - read_index));
|
||||
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result == HttpReadLoopResult::ERROR)
|
||||
return {HttpReadStatus::ERROR, read_bytes_or_error};
|
||||
if (result == HttpReadLoopResult::TIMEOUT)
|
||||
return {HttpReadStatus::TIMEOUT, 0};
|
||||
|
||||
read_index += read_bytes_or_error;
|
||||
}
|
||||
return {HttpReadStatus::OK, 0};
|
||||
}
|
||||
|
||||
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
|
||||
public:
|
||||
void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
|
||||
@@ -258,7 +124,6 @@ class HttpRequestComponent : public Component {
|
||||
|
||||
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
|
||||
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
uint32_t get_timeout() const { return this->timeout_; }
|
||||
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
|
||||
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
|
||||
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
|
||||
@@ -384,21 +249,15 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *buf = allocator.allocate(max_length);
|
||||
if (buf != nullptr) {
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
size_t read_index = 0;
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->parent_->get_timeout();
|
||||
while (container->get_bytes_read() < max_length) {
|
||||
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
if (read <= 0) {
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA)
|
||||
break; // ERROR or TIMEOUT
|
||||
read_index += read_or_error;
|
||||
read_index += read;
|
||||
}
|
||||
response_body.reserve(read_index);
|
||||
response_body.assign((char *) buf, read_index);
|
||||
|
||||
@@ -139,23 +139,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
return container;
|
||||
}
|
||||
|
||||
// Arduino HTTP read implementation
|
||||
//
|
||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||
//
|
||||
// Arduino's WiFiClient is inherently non-blocking - available() returns 0 when
|
||||
// no data is ready. We use connected() to distinguish "no data yet" from
|
||||
// "connection closed".
|
||||
//
|
||||
// WiFiClient behavior:
|
||||
// available() > 0: data ready to read
|
||||
// available() == 0 && connected(): no data yet, still connected
|
||||
// available() == 0 && !connected(): connection closed
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
|
||||
// > 0: bytes read
|
||||
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
|
||||
// < 0: error/connection closed <-- connection closed returns -1, not 0
|
||||
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
@@ -163,7 +146,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
WiFiClient *stream_ptr = this->client_.getStreamPtr();
|
||||
if (stream_ptr == nullptr) {
|
||||
ESP_LOGE(TAG, "Stream pointer vanished!");
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int available_data = stream_ptr->available();
|
||||
@@ -171,15 +154,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
// Check if we've read all expected content
|
||||
if (this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
// No data available - check if connection is still open
|
||||
if (!stream_ptr->connected()) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
|
||||
}
|
||||
return 0; // No data yet, caller should retry
|
||||
return 0;
|
||||
}
|
||||
|
||||
App.feed_wdt();
|
||||
|
||||
@@ -89,7 +89,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
config.max_redirection_count = this->redirect_limit_;
|
||||
config.auth_type = HTTP_AUTH_TYPE_BASIC;
|
||||
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||||
if (secure && this->verify_ssl_) {
|
||||
if (secure) {
|
||||
config.crt_bundle_attach = esp_crt_bundle_attach;
|
||||
}
|
||||
#endif
|
||||
@@ -209,57 +209,26 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
return container;
|
||||
}
|
||||
|
||||
// ESP-IDF HTTP read implementation (blocking mode)
|
||||
//
|
||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||
//
|
||||
// esp_http_client_read() in blocking mode returns:
|
||||
// > 0: bytes read
|
||||
// 0: connection closed (end of stream)
|
||||
// < 0: error
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract:
|
||||
// > 0: bytes read
|
||||
// 0: no data yet / all content read (caller should check bytes_read vs content_length)
|
||||
// < 0: error/connection closed
|
||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
// Check if we've already read all expected content
|
||||
if (this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
this->feed_wdt();
|
||||
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len);
|
||||
this->feed_wdt();
|
||||
if (read_len > 0) {
|
||||
this->bytes_read_ += read_len;
|
||||
}
|
||||
|
||||
this->feed_wdt();
|
||||
int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
|
||||
this->feed_wdt();
|
||||
|
||||
this->duration_ms += (millis() - start);
|
||||
|
||||
if (read_len_or_error > 0) {
|
||||
this->bytes_read_ += read_len_or_error;
|
||||
return read_len_or_error;
|
||||
}
|
||||
|
||||
// Connection closed by server before all content received
|
||||
if (read_len_or_error == 0) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
// Negative value - error, return the actual error code for debugging
|
||||
return read_len_or_error;
|
||||
return read_len;
|
||||
}
|
||||
|
||||
void HttpContainerIDF::end() {
|
||||
if (this->client_ == nullptr) {
|
||||
return; // Already cleaned up
|
||||
}
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
esp_http_client_close(this->client_);
|
||||
esp_http_client_cleanup(this->client_);
|
||||
this->client_ = nullptr;
|
||||
}
|
||||
|
||||
void HttpContainerIDF::feed_wdt() {
|
||||
|
||||
@@ -34,7 +34,6 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
|
||||
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
|
||||
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
|
||||
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
|
||||
|
||||
protected:
|
||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||
@@ -43,7 +42,6 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
|
||||
uint16_t buffer_size_rx_{};
|
||||
uint16_t buffer_size_tx_{};
|
||||
bool verify_ssl_{true};
|
||||
|
||||
/// @brief Monitors the http client events to gather response headers
|
||||
static esp_err_t http_event_handler(esp_http_client_event_t *evt);
|
||||
|
||||
@@ -115,47 +115,39 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
return error_code;
|
||||
}
|
||||
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->parent_->get_timeout();
|
||||
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
// read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
|
||||
int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
|
||||
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
|
||||
container->content_length, bufsize_or_error);
|
||||
// read a maximum of chunk_size bytes into buf. (real read size returned)
|
||||
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
|
||||
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
|
||||
container->content_length, bufsize);
|
||||
|
||||
// feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA) {
|
||||
if (result == HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading data");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (bufsize <= 0) {
|
||||
if (bufsize < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
}
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
// bufsize == 0: no more data available, exit loop
|
||||
break;
|
||||
}
|
||||
|
||||
// At this point bufsize_or_error > 0, so it's a valid size
|
||||
if (bufsize_or_error <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// add read bytes to MD5
|
||||
md5_receive.add(buf, bufsize_or_error);
|
||||
md5_receive.add(buf, bufsize);
|
||||
|
||||
// write bytes to OTA backend
|
||||
this->update_started_ = true;
|
||||
error_code = backend->write(buf, bufsize_or_error);
|
||||
error_code = backend->write(buf, bufsize);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
// error code explanation available at
|
||||
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
|
||||
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
|
||||
container->get_bytes_read() - bufsize_or_error, container->content_length);
|
||||
container->get_bytes_read() - bufsize, container->content_length);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return error_code;
|
||||
}
|
||||
@@ -252,19 +244,19 @@ bool OtaHttpRequestComponent::http_get_md5_() {
|
||||
}
|
||||
|
||||
this->md5_expected_.resize(MD5_SIZE);
|
||||
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
|
||||
this->parent_->get_timeout());
|
||||
int read_len = 0;
|
||||
while (container->get_bytes_read() < MD5_SIZE) {
|
||||
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
|
||||
if (read_len <= 0) {
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
}
|
||||
container->end();
|
||||
|
||||
if (result.status != HttpReadStatus::OK) {
|
||||
if (result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading MD5");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading MD5: %d", result.error_code);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
|
||||
return read_len == MD5_SIZE;
|
||||
}
|
||||
|
||||
bool OtaHttpRequestComponent::validate_url_(const std::string &url) {
|
||||
|
||||
@@ -11,12 +11,7 @@ namespace http_request {
|
||||
|
||||
// The update function runs in a task only on ESP32s.
|
||||
#ifdef USE_ESP32
|
||||
// vTaskDelete doesn't return, but clang-tidy doesn't know that
|
||||
#define UPDATE_RETURN \
|
||||
do { \
|
||||
vTaskDelete(nullptr); \
|
||||
__builtin_unreachable(); \
|
||||
} while (0)
|
||||
#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
|
||||
#else
|
||||
#define UPDATE_RETURN return
|
||||
#endif
|
||||
@@ -75,21 +70,19 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
|
||||
this_update->request_parent_->get_timeout());
|
||||
if (read_result.status != HttpReadStatus::OK) {
|
||||
if (read_result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading manifest");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
|
||||
size_t read_index = 0;
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
|
||||
|
||||
yield();
|
||||
|
||||
if (read_bytes <= 0) {
|
||||
// Network error or connection closed - break to avoid infinite loop
|
||||
break;
|
||||
}
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
|
||||
allocator.deallocate(data, container->content_length);
|
||||
container->end();
|
||||
UPDATE_RETURN;
|
||||
|
||||
read_index += read_bytes;
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
|
||||
bool valid = false;
|
||||
{ // Ensures the response string falls out of scope and deallocates before the task ends
|
||||
|
||||
@@ -42,8 +42,8 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t
|
||||
}
|
||||
|
||||
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const {
|
||||
SmallBufferWithHeapFallback<17> buffer_alloc(len + 1); // Most I2C writes are <= 16 bytes
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes
|
||||
uint8_t *buffer = buffer_alloc.get(len + 1);
|
||||
|
||||
buffer[0] = a_register;
|
||||
std::copy(data, data + len, buffer + 1);
|
||||
@@ -51,8 +51,8 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
|
||||
}
|
||||
|
||||
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const {
|
||||
SmallBufferWithHeapFallback<18> buffer_alloc(len + 2); // Most I2C writes are <= 16 bytes + 2 for register
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register
|
||||
uint8_t *buffer = buffer_alloc.get(len + 2);
|
||||
|
||||
buffer[0] = a_register >> 8;
|
||||
buffer[1] = a_register;
|
||||
|
||||
@@ -11,6 +11,22 @@
|
||||
namespace esphome {
|
||||
namespace i2c {
|
||||
|
||||
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
|
||||
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
|
||||
public:
|
||||
uint8_t *get(size_t size) {
|
||||
if (size <= STACK_SIZE) {
|
||||
return this->stack_buffer_;
|
||||
}
|
||||
this->heap_buffer_ = std::unique_ptr<uint8_t[]>(new uint8_t[size]);
|
||||
return this->heap_buffer_.get();
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t stack_buffer_[STACK_SIZE];
|
||||
std::unique_ptr<uint8_t[]> heap_buffer_;
|
||||
};
|
||||
|
||||
/// @brief Error codes returned by I2CBus and I2CDevice methods
|
||||
enum ErrorCode {
|
||||
NO_ERROR = 0, ///< No error found during execution of method
|
||||
@@ -76,8 +92,8 @@ class I2CBus {
|
||||
total_len += read_buffers[i].len;
|
||||
}
|
||||
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small
|
||||
uint8_t *buffer = buffer_alloc.get(total_len);
|
||||
|
||||
auto err = this->write_readv(address, nullptr, 0, buffer, total_len);
|
||||
if (err != ERROR_OK)
|
||||
@@ -100,8 +116,8 @@ class I2CBus {
|
||||
total_len += write_buffers[i].len;
|
||||
}
|
||||
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small
|
||||
uint8_t *buffer = buffer_alloc.get(total_len);
|
||||
|
||||
size_t pos = 0;
|
||||
for (size_t i = 0; i != count; i++) {
|
||||
|
||||
@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
|
||||
}
|
||||
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
|
||||
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
|
||||
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100);
|
||||
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20);
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
|
||||
@@ -223,7 +223,7 @@ async def to_code(config):
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
if init_sequences := config.get(CONF_INIT_SEQUENCE):
|
||||
|
||||
@@ -18,7 +18,15 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
|
||||
|
||||
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) {
|
||||
this->raw_timings_ = &timings;
|
||||
this->packed_data_ = nullptr; // Clear packed if vector is set
|
||||
this->packed_data_ = nullptr;
|
||||
this->base64url_ptr_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) {
|
||||
this->base64url_ptr_ = &base64url;
|
||||
this->raw_timings_ = nullptr;
|
||||
this->packed_data_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -26,7 +34,8 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t
|
||||
this->packed_data_ = data;
|
||||
this->packed_length_ = length;
|
||||
this->packed_count_ = count;
|
||||
this->raw_timings_ = nullptr; // Clear vector if packed is set
|
||||
this->raw_timings_ = nullptr;
|
||||
this->base64url_ptr_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -92,6 +101,23 @@ void Infrared::control(const InfraredCall &call) {
|
||||
call.get_packed_count());
|
||||
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(),
|
||||
call.get_repeat_count());
|
||||
} else if (call.is_base64url()) {
|
||||
// Decode base64url (URL-safe) into transmit buffer
|
||||
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
|
||||
ESP_LOGE(TAG, "Invalid base64url data");
|
||||
return;
|
||||
}
|
||||
// Sanity check: validate timing values are within reasonable bounds
|
||||
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
|
||||
for (int32_t timing : transmit_data->get_data()) {
|
||||
int32_t abs_timing = timing < 0 ? -timing : timing;
|
||||
if (abs_timing > max_timing_us) {
|
||||
ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(),
|
||||
call.get_repeat_count());
|
||||
} else {
|
||||
// From vector (lambdas/automations)
|
||||
transmit_data->set_data(call.get_raw_timings());
|
||||
|
||||
@@ -28,12 +28,29 @@ class InfraredCall {
|
||||
|
||||
/// Set the carrier frequency in Hz
|
||||
InfraredCall &set_carrier_frequency(uint32_t frequency);
|
||||
/// Set the raw timings (positive = mark, negative = space)
|
||||
/// Note: The timings vector must outlive the InfraredCall (zero-copy reference)
|
||||
|
||||
// ===== Raw Timings Methods =====
|
||||
// All set_raw_timings_* methods store pointers/references to external data.
|
||||
// The referenced data must remain valid until perform() completes.
|
||||
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
|
||||
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
|
||||
|
||||
/// Set the raw timings from a vector (positive = mark, negative = space)
|
||||
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
|
||||
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
|
||||
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
|
||||
/// Set the raw timings from packed protobuf sint32 data (zero-copy from wire)
|
||||
/// Note: The data must outlive the InfraredCall
|
||||
|
||||
/// Set the raw timings from base64url-encoded little-endian int32 data
|
||||
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
|
||||
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
|
||||
/// @note Decoding happens at perform() time, directly into the transmit buffer.
|
||||
InfraredCall &set_raw_timings_base64url(const std::string &base64url);
|
||||
|
||||
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
|
||||
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
|
||||
/// @note Usage: For API component where data comes directly from the protobuf message.
|
||||
InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count);
|
||||
|
||||
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
|
||||
InfraredCall &set_repeat_count(uint32_t count);
|
||||
|
||||
@@ -42,12 +59,18 @@ class InfraredCall {
|
||||
|
||||
/// Get the carrier frequency
|
||||
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
|
||||
/// Get the raw timings (only valid if set via set_raw_timings, not packed)
|
||||
/// Get the raw timings (only valid if set via set_raw_timings)
|
||||
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
|
||||
/// Check if raw timings have been set (either vector or packed)
|
||||
bool has_raw_timings() const { return this->raw_timings_ != nullptr || this->packed_data_ != nullptr; }
|
||||
/// Check if raw timings have been set (any format)
|
||||
bool has_raw_timings() const {
|
||||
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
|
||||
}
|
||||
/// Check if using packed data format
|
||||
bool is_packed() const { return this->packed_data_ != nullptr; }
|
||||
/// Check if using base64url data format
|
||||
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
|
||||
/// Get the base64url data string
|
||||
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
|
||||
/// Get packed data (only valid if set via set_raw_timings_packed)
|
||||
const uint8_t *get_packed_data() const { return this->packed_data_; }
|
||||
uint16_t get_packed_length() const { return this->packed_length_; }
|
||||
@@ -59,9 +82,11 @@ class InfraredCall {
|
||||
uint32_t repeat_count_{1};
|
||||
Infrared *parent_;
|
||||
optional<uint32_t> carrier_frequency_;
|
||||
// Vector-based timings (for lambdas/automations)
|
||||
// Pointer to vector-based timings (caller-owned, must outlive perform())
|
||||
const std::vector<int32_t> *raw_timings_{nullptr};
|
||||
// Packed protobuf timings (for API zero-copy)
|
||||
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
|
||||
const std::string *base64url_ptr_{nullptr};
|
||||
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
|
||||
const uint8_t *packed_data_{nullptr};
|
||||
uint16_t packed_length_{0};
|
||||
uint16_t packed_count_{0};
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Once the API is considered stable, this warning will be removed.
|
||||
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#include "esphome/components/remote_transmitter/remote_transmitter.h"
|
||||
#include "esphome/components/remote_receiver/remote_receiver.h"
|
||||
|
||||
namespace esphome::ir_rf_proxy {
|
||||
|
||||
|
||||
@@ -391,10 +391,7 @@ void LightCall::transform_parameters_() {
|
||||
min_mireds > 0.0f && max_mireds > 0.0f) {
|
||||
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
|
||||
this->parent_->get_name().c_str());
|
||||
// Only compute cold_white/warm_white from color_temperature if they're not already explicitly set.
|
||||
// This is important for state restoration, where both color_temperature and cold_white/warm_white
|
||||
// are restored from flash - we want to preserve the saved cold_white/warm_white values.
|
||||
if (this->has_color_temperature() && !this->has_cold_white() && !this->has_warm_white()) {
|
||||
if (this->has_color_temperature()) {
|
||||
const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds);
|
||||
const float range = max_mireds - min_mireds;
|
||||
const float ww_fraction = (color_temp - min_mireds) / range;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "light_json_schema.h"
|
||||
#include "color_mode.h"
|
||||
#include "light_output.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
@@ -8,29 +9,32 @@ namespace esphome::light {
|
||||
|
||||
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
||||
|
||||
// Get JSON string for color mode using linear search (avoids large switch jump table)
|
||||
static const char *get_color_mode_json_str(ColorMode mode) {
|
||||
// Parallel arrays: mode values and their corresponding strings
|
||||
// Uses less RAM than a switch jump table on sparse enum values
|
||||
static constexpr ColorMode MODES[] = {
|
||||
ColorMode::ON_OFF,
|
||||
ColorMode::BRIGHTNESS,
|
||||
ColorMode::WHITE,
|
||||
ColorMode::COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB,
|
||||
ColorMode::RGB_WHITE,
|
||||
ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE,
|
||||
};
|
||||
static constexpr const char *STRINGS[] = {
|
||||
"onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", "rgbww",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(MODES) / sizeof(MODES[0]); i++) {
|
||||
if (MODES[i] == mode)
|
||||
return STRINGS[i];
|
||||
// Get JSON string for color mode.
|
||||
// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would
|
||||
// generate a large jump table. Converting to bit index (0-9) allows a compact switch.
|
||||
static ProgmemStr get_color_mode_json_str(ColorMode mode) {
|
||||
switch (ColorModeBitPolicy::to_bit(mode)) {
|
||||
case 1:
|
||||
return ESPHOME_F("onoff");
|
||||
case 2:
|
||||
return ESPHOME_F("brightness");
|
||||
case 3:
|
||||
return ESPHOME_F("white");
|
||||
case 4:
|
||||
return ESPHOME_F("color_temp");
|
||||
case 5:
|
||||
return ESPHOME_F("cwww");
|
||||
case 6:
|
||||
return ESPHOME_F("rgb");
|
||||
case 7:
|
||||
return ESPHOME_F("rgbw");
|
||||
case 8:
|
||||
return ESPHOME_F("rgbct");
|
||||
case 9:
|
||||
return ESPHOME_F("rgbww");
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
@@ -44,7 +48,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
auto values = state.remote_values;
|
||||
|
||||
const auto color_mode = values.get_color_mode();
|
||||
const char *mode_str = get_color_mode_json_str(color_mode);
|
||||
const auto *mode_str = get_color_mode_json_str(color_mode);
|
||||
if (mode_str != nullptr) {
|
||||
root[ESPHOME_F("color_mode")] = mode_str;
|
||||
}
|
||||
|
||||
@@ -413,6 +413,7 @@ class TextValidator(LValidator):
|
||||
str_args = [str(x) for x in value[CONF_ARGS]]
|
||||
arg_expr = cg.RawExpression(",".join(str_args))
|
||||
format_str = cpp_string_escape(format_str)
|
||||
# str_sprintf justified: user-defined format, can't optimize without permanent RAM cost
|
||||
sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()"
|
||||
if nanval := value.get(CONF_IF_NAN):
|
||||
nanval = cpp_string_escape(nanval)
|
||||
|
||||
@@ -65,7 +65,10 @@ std::string lv_event_code_name_for(uint8_t event_code) {
|
||||
if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) {
|
||||
return EVENT_NAMES[event_code];
|
||||
}
|
||||
return str_sprintf("%2d", event_code);
|
||||
// max 4 bytes: "%u" with uint8_t (max 255, 3 digits) + null
|
||||
char buf[4];
|
||||
snprintf(buf, sizeof(buf), "%u", event_code);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from esphome import codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_OPTIONS
|
||||
|
||||
@@ -25,34 +24,6 @@ from .label import CONF_LABEL
|
||||
CONF_DROPDOWN = "dropdown"
|
||||
CONF_DROPDOWN_LIST = "dropdown_list"
|
||||
|
||||
# Example valid dropdown symbol (left arrow) for error messages
|
||||
EXAMPLE_DROPDOWN_SYMBOL = "\U00002190" # ←
|
||||
|
||||
|
||||
def dropdown_symbol_validator(value):
|
||||
"""
|
||||
Validate that the dropdown symbol is a single Unicode character
|
||||
with a codepoint of 0x100 (256) or greater.
|
||||
This is required because LVGL uses codepoints below 0x100 for internal symbols.
|
||||
"""
|
||||
value = cv.string(value)
|
||||
# len(value) counts Unicode code points, not grapheme clusters or bytes
|
||||
if len(value) != 1:
|
||||
raise cv.Invalid(
|
||||
f"Dropdown symbol must be a single character, got '{value}' with length {len(value)}"
|
||||
)
|
||||
codepoint = ord(value)
|
||||
if codepoint < 0x100:
|
||||
# Format the example symbol as a Unicode escape for the error message
|
||||
example_escape = f"\\U{ord(EXAMPLE_DROPDOWN_SYMBOL):08X}"
|
||||
raise cv.Invalid(
|
||||
f"Dropdown symbol must have a Unicode codepoint of 0x100 (256) or greater. "
|
||||
f"'{value}' has codepoint {codepoint} (0x{codepoint:X}). "
|
||||
f"Use a character like '{example_escape}' ({EXAMPLE_DROPDOWN_SYMBOL}) or other Unicode symbols with codepoint >= 0x100."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
lv_dropdown_t = LvSelect("LvDropdownType", parents=(LvCompound,))
|
||||
|
||||
lv_dropdown_list_t = LvType("lv_dropdown_list_t")
|
||||
@@ -62,7 +33,7 @@ dropdown_list_spec = WidgetType(
|
||||
|
||||
DROPDOWN_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SYMBOL): dropdown_symbol_validator,
|
||||
cv.Optional(CONF_SYMBOL): lv_text,
|
||||
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
|
||||
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
|
||||
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
|
||||
@@ -99,7 +70,7 @@ class DropdownType(WidgetType):
|
||||
if options := config.get(CONF_OPTIONS):
|
||||
lv_add(w.var.set_options(options))
|
||||
if symbol := config.get(CONF_SYMBOL):
|
||||
lv.dropdown_set_symbol(w.var.obj, cg.safe_exp(symbol))
|
||||
lv.dropdown_set_symbol(w.var.obj, await lv_text.process(symbol))
|
||||
if (selected := config.get(CONF_SELECTED_INDEX)) is not None:
|
||||
value = await lv_int.process(selected)
|
||||
lv_add(w.var.set_selected_index(value, literal("LV_ANIM_OFF")))
|
||||
|
||||
@@ -32,7 +32,7 @@ class LabelType(WidgetType):
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
"""For a text object, create and set text"""
|
||||
if (value := config.get(CONF_TEXT)) is not None:
|
||||
if value := config.get(CONF_TEXT):
|
||||
await w.set_property(CONF_TEXT, await lv_text.process(value))
|
||||
await w.set_property(CONF_LONG_MODE, config)
|
||||
await w.set_property(CONF_RECOLOR, config)
|
||||
|
||||
@@ -29,7 +29,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
await display.register_display(var, config)
|
||||
|
||||
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
|
||||
|
||||
@@ -86,7 +86,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
await display.register_display(var, config)
|
||||
|
||||
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
|
||||
|
||||
@@ -101,4 +101,225 @@ DriverChip(
|
||||
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x00),
|
||||
]
|
||||
)
|
||||
|
||||
# jc8012P4A1 Driver Configuration (jd9365)
|
||||
# Using parameters from esp_lcd_jd9365.h and the working full init sequence
|
||||
# ----------------------------------------------------------------------------------------------------------------------
|
||||
# * Resolution: 800x1280
|
||||
# * PCLK Frequency: 60 MHz
|
||||
# * DSI Lane Bit Rate: 1 Gbps (using 2-Lane DSI configuration)
|
||||
# * Horizontal Timing (hsync_pulse_width=20, hsync_back_porch=20, hsync_front_porch=40)
|
||||
# * Vertical Timing (vsync_pulse_width=4, vsync_back_porch=8, vsync_front_porch=20)
|
||||
# ----------------------------------------------------------------------------------------------------------------------
|
||||
DriverChip(
|
||||
"JC8012P4A1",
|
||||
width=800,
|
||||
height=1280,
|
||||
hsync_back_porch=20,
|
||||
hsync_pulse_width=20,
|
||||
hsync_front_porch=40,
|
||||
vsync_back_porch=8,
|
||||
vsync_pulse_width=4,
|
||||
vsync_front_porch=20,
|
||||
pclk_frequency="60MHz",
|
||||
lane_bit_rate="1Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
reset_pin=27,
|
||||
initsequence=[
|
||||
(0xE0, 0x00),
|
||||
(0xE1, 0x93),
|
||||
(0xE2, 0x65),
|
||||
(0xE3, 0xF8),
|
||||
(0x80, 0x01),
|
||||
(0xE0, 0x01),
|
||||
(0x00, 0x00),
|
||||
(0x01, 0x39),
|
||||
(0x03, 0x10),
|
||||
(0x04, 0x41),
|
||||
(0x0C, 0x74),
|
||||
(0x17, 0x00),
|
||||
(0x18, 0xD7),
|
||||
(0x19, 0x00),
|
||||
(0x1A, 0x00),
|
||||
(0x1B, 0xD7),
|
||||
(0x1C, 0x00),
|
||||
(0x24, 0xFE),
|
||||
(0x35, 0x26),
|
||||
(0x37, 0x69),
|
||||
(0x38, 0x05),
|
||||
(0x39, 0x06),
|
||||
(0x3A, 0x08),
|
||||
(0x3C, 0x78),
|
||||
(0x3D, 0xFF),
|
||||
(0x3E, 0xFF),
|
||||
(0x3F, 0xFF),
|
||||
(0x40, 0x06),
|
||||
(0x41, 0xA0),
|
||||
(0x43, 0x14),
|
||||
(0x44, 0x0B),
|
||||
(0x45, 0x30),
|
||||
(0x4B, 0x04),
|
||||
(0x55, 0x02),
|
||||
(0x57, 0x89),
|
||||
(0x59, 0x0A),
|
||||
(0x5A, 0x28),
|
||||
(0x5B, 0x15),
|
||||
(0x5D, 0x50),
|
||||
(0x5E, 0x37),
|
||||
(0x5F, 0x29),
|
||||
(0x60, 0x1E),
|
||||
(0x61, 0x1D),
|
||||
(0x62, 0x12),
|
||||
(0x63, 0x1A),
|
||||
(0x64, 0x08),
|
||||
(0x65, 0x25),
|
||||
(0x66, 0x26),
|
||||
(0x67, 0x28),
|
||||
(0x68, 0x49),
|
||||
(0x69, 0x3A),
|
||||
(0x6A, 0x43),
|
||||
(0x6B, 0x3A),
|
||||
(0x6C, 0x3B),
|
||||
(0x6D, 0x32),
|
||||
(0x6E, 0x1F),
|
||||
(0x6F, 0x0E),
|
||||
(0x70, 0x50),
|
||||
(0x71, 0x37),
|
||||
(0x72, 0x29),
|
||||
(0x73, 0x1E),
|
||||
(0x74, 0x1D),
|
||||
(0x75, 0x12),
|
||||
(0x76, 0x1A),
|
||||
(0x77, 0x08),
|
||||
(0x78, 0x25),
|
||||
(0x79, 0x26),
|
||||
(0x7A, 0x28),
|
||||
(0x7B, 0x49),
|
||||
(0x7C, 0x3A),
|
||||
(0x7D, 0x43),
|
||||
(0x7E, 0x3A),
|
||||
(0x7F, 0x3B),
|
||||
(0x80, 0x32),
|
||||
(0x81, 0x1F),
|
||||
(0x82, 0x0E),
|
||||
(0xE0, 0x02),
|
||||
(0x00, 0x1F),
|
||||
(0x01, 0x1F),
|
||||
(0x02, 0x52),
|
||||
(0x03, 0x51),
|
||||
(0x04, 0x50),
|
||||
(0x05, 0x4B),
|
||||
(0x06, 0x4A),
|
||||
(0x07, 0x49),
|
||||
(0x08, 0x48),
|
||||
(0x09, 0x47),
|
||||
(0x0A, 0x46),
|
||||
(0x0B, 0x45),
|
||||
(0x0C, 0x44),
|
||||
(0x0D, 0x40),
|
||||
(0x0E, 0x41),
|
||||
(0x0F, 0x1F),
|
||||
(0x10, 0x1F),
|
||||
(0x11, 0x1F),
|
||||
(0x12, 0x1F),
|
||||
(0x13, 0x1F),
|
||||
(0x14, 0x1F),
|
||||
(0x15, 0x1F),
|
||||
(0x16, 0x1F),
|
||||
(0x17, 0x1F),
|
||||
(0x18, 0x52),
|
||||
(0x19, 0x51),
|
||||
(0x1A, 0x50),
|
||||
(0x1B, 0x4B),
|
||||
(0x1C, 0x4A),
|
||||
(0x1D, 0x49),
|
||||
(0x1E, 0x48),
|
||||
(0x1F, 0x47),
|
||||
(0x20, 0x46),
|
||||
(0x21, 0x45),
|
||||
(0x22, 0x44),
|
||||
(0x23, 0x40),
|
||||
(0x24, 0x41),
|
||||
(0x25, 0x1F),
|
||||
(0x26, 0x1F),
|
||||
(0x27, 0x1F),
|
||||
(0x28, 0x1F),
|
||||
(0x29, 0x1F),
|
||||
(0x2A, 0x1F),
|
||||
(0x2B, 0x1F),
|
||||
(0x2C, 0x1F),
|
||||
(0x2D, 0x1F),
|
||||
(0x2E, 0x52),
|
||||
(0x2F, 0x40),
|
||||
(0x30, 0x41),
|
||||
(0x31, 0x48),
|
||||
(0x32, 0x49),
|
||||
(0x33, 0x4A),
|
||||
(0x34, 0x4B),
|
||||
(0x35, 0x44),
|
||||
(0x36, 0x45),
|
||||
(0x37, 0x46),
|
||||
(0x38, 0x47),
|
||||
(0x39, 0x51),
|
||||
(0x3A, 0x50),
|
||||
(0x3B, 0x1F),
|
||||
(0x3C, 0x1F),
|
||||
(0x3D, 0x1F),
|
||||
(0x3E, 0x1F),
|
||||
(0x3F, 0x1F),
|
||||
(0x40, 0x1F),
|
||||
(0x41, 0x1F),
|
||||
(0x42, 0x1F),
|
||||
(0x43, 0x1F),
|
||||
(0x44, 0x52),
|
||||
(0x45, 0x40),
|
||||
(0x46, 0x41),
|
||||
(0x47, 0x48),
|
||||
(0x48, 0x49),
|
||||
(0x49, 0x4A),
|
||||
(0x4A, 0x4B),
|
||||
(0x4B, 0x44),
|
||||
(0x4C, 0x45),
|
||||
(0x4D, 0x46),
|
||||
(0x4E, 0x47),
|
||||
(0x4F, 0x51),
|
||||
(0x50, 0x50),
|
||||
(0x51, 0x1F),
|
||||
(0x52, 0x1F),
|
||||
(0x53, 0x1F),
|
||||
(0x54, 0x1F),
|
||||
(0x55, 0x1F),
|
||||
(0x56, 0x1F),
|
||||
(0x57, 0x1F),
|
||||
(0x58, 0x40),
|
||||
(0x59, 0x00),
|
||||
(0x5A, 0x00),
|
||||
(0x5B, 0x10),
|
||||
(0x5C, 0x05),
|
||||
(0x5D, 0x50),
|
||||
(0x5E, 0x01),
|
||||
(0x5F, 0x02),
|
||||
(0x60, 0x50),
|
||||
(0x61, 0x06),
|
||||
(0x62, 0x04),
|
||||
(0x63, 0x03),
|
||||
(0x64, 0x64),
|
||||
(0x65, 0x65),
|
||||
(0x66, 0x0B),
|
||||
(0x67, 0x73),
|
||||
(0x68, 0x07),
|
||||
(0x69, 0x06),
|
||||
(0x6A, 0x64),
|
||||
(0x6B, 0x08),
|
||||
(0x6C, 0x00),
|
||||
(0x6D, 0x32),
|
||||
(0x6E, 0x08),
|
||||
(0xE0, 0x04),
|
||||
(0x2C, 0x6B),
|
||||
(0x35, 0x08),
|
||||
(0x37, 0x00),
|
||||
(0xE0, 0x00),
|
||||
]
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
@@ -260,7 +260,7 @@ async def to_code(config):
|
||||
cg.add(var.set_enable_pins(enable))
|
||||
|
||||
if CONF_SPI_ID in config:
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
sequence, madctl = model.get_sequence(config)
|
||||
cg.add(var.set_init_sequence(sequence))
|
||||
cg.add(var.set_madctl(madctl))
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "mipi_rgb.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
#include <span>
|
||||
|
||||
namespace esphome {
|
||||
namespace mipi_rgb {
|
||||
@@ -345,27 +343,19 @@ int MipiRgb::get_height() {
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_pin_name(GPIOPin *pin, std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
|
||||
static std::string get_pin_name(GPIOPin *pin) {
|
||||
if (pin == nullptr)
|
||||
return "None";
|
||||
pin->dump_summary(buffer.data(), buffer.size());
|
||||
return buffer.data();
|
||||
return pin->dump_summary();
|
||||
}
|
||||
|
||||
void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) {
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
for (uint8_t i = start; i != end; i++) {
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, pin_summary);
|
||||
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void MipiRgb::dump_config() {
|
||||
char reset_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char de_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char pclk_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char hsync_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char vsync_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"MIPI_RGB LCD"
|
||||
"\n Model: %s"
|
||||
@@ -389,9 +379,9 @@ void MipiRgb::dump_config() {
|
||||
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_, reset_buf),
|
||||
get_pin_name(this->de_pin_, de_buf), get_pin_name(this->pclk_pin_, pclk_buf),
|
||||
get_pin_name(this->hsync_pin_, hsync_buf), get_pin_name(this->vsync_pin_, vsync_buf));
|
||||
(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());
|
||||
|
||||
this->dump_pins_(8, 13, "Blue", 0);
|
||||
this->dump_pins_(13, 16, "Green", 0);
|
||||
|
||||
@@ -55,7 +55,6 @@ st7701s = ST7701S(
|
||||
pclk_frequency="16MHz",
|
||||
pclk_inverted=True,
|
||||
initsequence=(
|
||||
(0x01,), # Software Reset
|
||||
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0
|
||||
(0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05),
|
||||
(0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,),
|
||||
|
||||
@@ -443,4 +443,6 @@ async def to_code(config):
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
# Displays are write-only, set the SPI device to write-only as well
|
||||
cg.add(var.set_write_only(True))
|
||||
|
||||
@@ -279,7 +279,7 @@ def modbus_calc_properties(config):
|
||||
if isinstance(value, str):
|
||||
value = value.encode()
|
||||
config[CONF_ADDRESS] = binascii.crc_hqx(value, 0)
|
||||
config[CONF_REGISTER_TYPE] = cv.enum(MODBUS_REGISTER_TYPE)("custom")
|
||||
config[CONF_REGISTER_TYPE] = ModbusRegisterType.CUSTOM
|
||||
config[CONF_FORCE_NEW_RANGE] = True
|
||||
return byte_offset, reg_count
|
||||
|
||||
|
||||
@@ -271,24 +271,31 @@ class ServerRegister {
|
||||
|
||||
// Formats a raw value into a string representation based on the value type for debugging
|
||||
std::string format_value(int64_t value) const {
|
||||
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
|
||||
// plus null terminator = 43, rounded to 44 for 4-byte alignment
|
||||
char buf[44];
|
||||
switch (this->value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::U_DWORD_R:
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::U_QWORD_R:
|
||||
return std::to_string(static_cast<uint64_t>(value));
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
|
||||
return buf;
|
||||
case SensorValueType::S_WORD:
|
||||
case SensorValueType::S_DWORD:
|
||||
case SensorValueType::S_DWORD_R:
|
||||
case SensorValueType::S_QWORD:
|
||||
case SensorValueType::S_QWORD_R:
|
||||
return std::to_string(value);
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||
return buf;
|
||||
case SensorValueType::FP32_R:
|
||||
case SensorValueType::FP32:
|
||||
return str_sprintf("%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
|
||||
return buf;
|
||||
default:
|
||||
return std::to_string(value);
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,20 @@ void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) {
|
||||
while ((items_left > 0) && index < data.size()) {
|
||||
uint8_t b = data[index];
|
||||
switch (this->encode_) {
|
||||
case RawEncoding::HEXBYTES:
|
||||
output_str += str_snprintf("%02x", 2, b);
|
||||
case RawEncoding::HEXBYTES: {
|
||||
// max 3: 2 hex digits + null
|
||||
char hex_buf[3];
|
||||
snprintf(hex_buf, sizeof(hex_buf), "%02x", b);
|
||||
output_str += hex_buf;
|
||||
break;
|
||||
case RawEncoding::COMMA:
|
||||
output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b);
|
||||
}
|
||||
case RawEncoding::COMMA: {
|
||||
// max 5: optional ','(1) + uint8(3) + null, for both ",%d" and "%d"
|
||||
char dec_buf[5];
|
||||
snprintf(dec_buf, sizeof(dec_buf), index != this->offset ? ",%d" : "%d", b);
|
||||
output_str += dec_buf;
|
||||
break;
|
||||
}
|
||||
case RawEncoding::ANSI:
|
||||
if (b < 0x20)
|
||||
break;
|
||||
|
||||
@@ -189,8 +189,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
|
||||
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
|
||||
char friendly_name_hash[9];
|
||||
sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_()));
|
||||
friendly_name_hash[8] = 0; // ensure the hash-string ends with null
|
||||
snprintf(friendly_name_hash, sizeof(friendly_name_hash), "%08" PRIx32, fnv1_hash(this->friendly_name_()));
|
||||
// Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
|
||||
// MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
|
||||
char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11];
|
||||
|
||||
@@ -561,8 +561,9 @@ const char *OpenTherm::message_id_to_str(MessageId id) {
|
||||
}
|
||||
|
||||
void OpenTherm::debug_data(OpenthermData &data) {
|
||||
ESP_LOGD(TAG, "%s %s %s %s", format_bin(data.type).c_str(), format_bin(data.id).c_str(),
|
||||
format_bin(data.valueHB).c_str(), format_bin(data.valueLB).c_str());
|
||||
char type_buf[9], id_buf[9], hb_buf[9], lb_buf[9];
|
||||
ESP_LOGD(TAG, "%s %s %s %s", format_bin_to(type_buf, data.type), format_bin_to(id_buf, data.id),
|
||||
format_bin_to(hb_buf, data.valueHB), format_bin_to(lb_buf, data.valueLB));
|
||||
ESP_LOGD(TAG, "type: %s; id: %u; HB: %u; LB: %u; uint_16: %u; float: %f",
|
||||
this->message_type_to_str((MessageType) data.type), data.id, data.valueHB, data.valueLB, data.u16(),
|
||||
data.f88());
|
||||
|
||||
@@ -44,7 +44,7 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "pipsolar.output";
|
||||
|
||||
void PipsolarOutput::write_state(float state) {
|
||||
char tmp[10];
|
||||
sprintf(tmp, this->set_command_.c_str(), state);
|
||||
snprintf(tmp, sizeof(tmp), this->set_command_, state);
|
||||
|
||||
if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
|
||||
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);
|
||||
|
||||
@@ -15,13 +15,15 @@ class PipsolarOutput : public output::FloatOutput {
|
||||
public:
|
||||
PipsolarOutput() {}
|
||||
void set_parent(Pipsolar *parent) { this->parent_ = parent; }
|
||||
void set_set_command(const std::string &command) { this->set_command_ = command; };
|
||||
void set_set_command(const char *command) { this->set_command_ = command; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_set_command(const std::string &command) = delete;
|
||||
void set_possible_values(std::vector<float> possible_values) { this->possible_values_ = std::move(possible_values); }
|
||||
void set_value(float value) { this->write_state(value); };
|
||||
void set_value(float value) { this->write_state(value); }
|
||||
|
||||
protected:
|
||||
void write_state(float state) override;
|
||||
std::string set_command_;
|
||||
const char *set_command_{nullptr};
|
||||
Pipsolar *parent_;
|
||||
std::vector<float> possible_values_;
|
||||
};
|
||||
|
||||
@@ -9,14 +9,9 @@ static const char *const TAG = "pipsolar.switch";
|
||||
|
||||
void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); }
|
||||
void PipsolarSwitch::write_state(bool state) {
|
||||
if (state) {
|
||||
if (!this->on_command_.empty()) {
|
||||
this->parent_->queue_command(this->on_command_);
|
||||
}
|
||||
} else {
|
||||
if (!this->off_command_.empty()) {
|
||||
this->parent_->queue_command(this->off_command_);
|
||||
}
|
||||
const char *command = state ? this->on_command_ : this->off_command_;
|
||||
if (command != nullptr) {
|
||||
this->parent_->queue_command(command);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,18 @@ namespace pipsolar {
|
||||
class Pipsolar;
|
||||
class PipsolarSwitch : public switch_::Switch, public Component {
|
||||
public:
|
||||
void set_parent(Pipsolar *parent) { this->parent_ = parent; };
|
||||
void set_on_command(const std::string &command) { this->on_command_ = command; };
|
||||
void set_off_command(const std::string &command) { this->off_command_ = command; };
|
||||
void set_parent(Pipsolar *parent) { this->parent_ = parent; }
|
||||
void set_on_command(const char *command) { this->on_command_ = command; }
|
||||
void set_off_command(const char *command) { this->off_command_ = command; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_on_command(const std::string &command) = delete;
|
||||
void set_off_command(const std::string &command) = delete;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
std::string on_command_;
|
||||
std::string off_command_;
|
||||
const char *on_command_{nullptr};
|
||||
const char *off_command_{nullptr};
|
||||
Pipsolar *parent_;
|
||||
};
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
chip = DriverChip.chips[config[CONF_MODEL]]
|
||||
if chip.initsequence:
|
||||
|
||||
@@ -133,17 +133,14 @@ void RD03DComponent::process_frame_() {
|
||||
uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE);
|
||||
|
||||
// Extract raw bytes for this target
|
||||
// Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution,
|
||||
// actual radar output has Resolution before Speed (verified empirically -
|
||||
// stationary targets were showing non-zero speed with original field order)
|
||||
uint8_t x_low = this->buffer_[offset + 0];
|
||||
uint8_t x_high = this->buffer_[offset + 1];
|
||||
uint8_t y_low = this->buffer_[offset + 2];
|
||||
uint8_t y_high = this->buffer_[offset + 3];
|
||||
uint8_t res_low = this->buffer_[offset + 4];
|
||||
uint8_t res_high = this->buffer_[offset + 5];
|
||||
uint8_t speed_low = this->buffer_[offset + 6];
|
||||
uint8_t speed_high = this->buffer_[offset + 7];
|
||||
uint8_t speed_low = this->buffer_[offset + 4];
|
||||
uint8_t speed_high = this->buffer_[offset + 5];
|
||||
uint8_t res_low = this->buffer_[offset + 6];
|
||||
uint8_t res_high = this->buffer_[offset + 7];
|
||||
|
||||
// Decode values per RD-03D format
|
||||
int16_t x = decode_value(x_low, x_high);
|
||||
|
||||
@@ -85,8 +85,8 @@ optional<AEHAData> AEHAProtocol::decode(RemoteReceiveData src) {
|
||||
std::string AEHAProtocol::format_data_(const std::vector<uint8_t> &data) {
|
||||
std::string out;
|
||||
for (uint8_t byte : data) {
|
||||
char buf[6];
|
||||
sprintf(buf, "0x%02X,", byte);
|
||||
char buf[8]; // "0x%02X," = 5 chars + null + margin
|
||||
snprintf(buf, sizeof(buf), "0x%02X,", byte);
|
||||
out += buf;
|
||||
}
|
||||
out.pop_back();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "raw_protocol.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -8,36 +9,30 @@ static const char *const TAG = "remote.raw";
|
||||
|
||||
bool RawDumper::dump(RemoteReceiveData src) {
|
||||
char buffer[256];
|
||||
uint32_t buffer_offset = 0;
|
||||
buffer_offset += sprintf(buffer, "Received Raw: ");
|
||||
size_t pos = buf_append_printf(buffer, sizeof(buffer), 0, "Received Raw: ");
|
||||
|
||||
for (int32_t i = 0; i < src.size() - 1; i++) {
|
||||
const int32_t value = src[i];
|
||||
const uint32_t remaining_length = sizeof(buffer) - buffer_offset;
|
||||
int written;
|
||||
size_t prev_pos = pos;
|
||||
|
||||
if (i + 1 < src.size() - 1) {
|
||||
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32 ", ", value);
|
||||
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32 ", ", value);
|
||||
} else {
|
||||
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32, value);
|
||||
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32, value);
|
||||
}
|
||||
|
||||
if (written < 0 || written >= int(remaining_length)) {
|
||||
// write failed, flush...
|
||||
buffer[buffer_offset] = '\0';
|
||||
if (pos >= sizeof(buffer) - 1) {
|
||||
// buffer full, flush and continue
|
||||
buffer[prev_pos] = '\0';
|
||||
ESP_LOGI(TAG, "%s", buffer);
|
||||
buffer_offset = 0;
|
||||
written = sprintf(buffer, " ");
|
||||
if (i + 1 < src.size() - 1) {
|
||||
written += sprintf(buffer + written, "%" PRId32 ", ", value);
|
||||
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32 ", ", value);
|
||||
} else {
|
||||
written += sprintf(buffer + written, "%" PRId32, value);
|
||||
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32, value);
|
||||
}
|
||||
}
|
||||
|
||||
buffer_offset += written;
|
||||
}
|
||||
if (buffer_offset != 0) {
|
||||
if (pos != 0) {
|
||||
ESP_LOGI(TAG, "%s", buffer);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#include "remote_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace remote_base {
|
||||
|
||||
@@ -159,42 +158,41 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t
|
||||
}
|
||||
}
|
||||
|
||||
bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) {
|
||||
return base64_decode_int32_vector(base64url, this->data_);
|
||||
}
|
||||
|
||||
/* RemoteTransmitterBase */
|
||||
|
||||
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
const auto &vec = this->temp_.get_data();
|
||||
char buffer[256];
|
||||
uint32_t buffer_offset = 0;
|
||||
buffer_offset += sprintf(buffer, "Sending times=%" PRIu32 " wait=%" PRIu32 "ms: ", send_times, send_wait);
|
||||
size_t pos = buf_append_printf(buffer, sizeof(buffer), 0,
|
||||
"Sending times=%" PRIu32 " wait=%" PRIu32 "ms: ", send_times, send_wait);
|
||||
|
||||
for (size_t i = 0; i < vec.size(); i++) {
|
||||
const int32_t value = vec[i];
|
||||
const uint32_t remaining_length = sizeof(buffer) - buffer_offset;
|
||||
int written;
|
||||
size_t prev_pos = pos;
|
||||
|
||||
if (i + 1 < vec.size()) {
|
||||
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32 ", ", value);
|
||||
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32 ", ", value);
|
||||
} else {
|
||||
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32, value);
|
||||
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32, value);
|
||||
}
|
||||
|
||||
if (written < 0 || written >= int(remaining_length)) {
|
||||
// write failed, flush...
|
||||
buffer[buffer_offset] = '\0';
|
||||
if (pos >= sizeof(buffer) - 1) {
|
||||
// buffer full, flush and continue
|
||||
buffer[prev_pos] = '\0';
|
||||
ESP_LOGVV(TAG, "%s", buffer);
|
||||
buffer_offset = 0;
|
||||
written = sprintf(buffer, " ");
|
||||
if (i + 1 < vec.size()) {
|
||||
written += sprintf(buffer + written, "%" PRId32 ", ", value);
|
||||
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32 ", ", value);
|
||||
} else {
|
||||
written += sprintf(buffer + written, "%" PRId32, value);
|
||||
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32, value);
|
||||
}
|
||||
}
|
||||
|
||||
buffer_offset += written;
|
||||
}
|
||||
if (buffer_offset != 0) {
|
||||
if (pos != 0) {
|
||||
ESP_LOGVV(TAG, "%s", buffer);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -36,6 +36,11 @@ class RemoteTransmitData {
|
||||
/// @param len Length of the buffer in bytes
|
||||
/// @param count Number of values (for reserve optimization)
|
||||
void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count);
|
||||
/// Set data from base64url-encoded little-endian int32 values
|
||||
/// Base64url is URL-safe: uses '-' instead of '+', '_' instead of '/'
|
||||
/// @param base64url Base64url-encoded string of little-endian int32 values
|
||||
/// @return true if successful, false if decode failed or invalid size
|
||||
bool set_data_from_base64url(const std::string &base64url);
|
||||
void reset() {
|
||||
this->data_.clear();
|
||||
this->carrier_frequency_ = 0;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "rpi_dpi_rgb.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -135,11 +134,8 @@ void RpiDpiRgb::dump_config() {
|
||||
LOG_PIN(" Enable Pin: ", this->enable_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
for (size_t i = 0; i != data_pin_count; i++) {
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
|
||||
}
|
||||
for (size_t i = 0; i != data_pin_count; i++)
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
|
||||
}
|
||||
|
||||
void RpiDpiRgb::reset_display_() const {
|
||||
|
||||
@@ -8,17 +8,20 @@ from esphome.const import (
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_INDEX,
|
||||
CONF_LAMBDA,
|
||||
CONF_MODE,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_VALUE,
|
||||
CONF_OPERATION,
|
||||
CONF_OPTION,
|
||||
CONF_OPTIONS,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_generator import MockObjClass, TemplateArguments
|
||||
from esphome.cpp_types import global_ns
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -38,6 +41,9 @@ SelectSetAction = select_ns.class_("SelectSetAction", automation.Action)
|
||||
SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action)
|
||||
SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action)
|
||||
|
||||
# Conditions
|
||||
SelectIsCondition = select_ns.class_("SelectIsCondition", automation.Condition)
|
||||
|
||||
# Enums
|
||||
SelectOperation = select_ns.enum("SelectOperation")
|
||||
SELECT_OPERATION_OPTIONS = {
|
||||
@@ -165,6 +171,41 @@ async def select_set_index_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"select.is",
|
||||
SelectIsCondition,
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_OPTIONS): cv.All(
|
||||
cv.ensure_list(cv.string_strict), cv.Length(min=1)
|
||||
),
|
||||
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
|
||||
}
|
||||
).add_extra(cv.has_exactly_one_key(CONF_OPTIONS, CONF_LAMBDA)),
|
||||
)
|
||||
async def select_is_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
if options := config.get(CONF_OPTIONS):
|
||||
# List of constant options
|
||||
# Create a constexpr and pass that with a template length
|
||||
arr_id = ID(
|
||||
f"{condition_id}_data",
|
||||
is_declaration=True,
|
||||
type=global_ns.namespace("constexpr char * const"),
|
||||
)
|
||||
arg = cg.static_const_array(arr_id, cg.ArrayInitializer(*options))
|
||||
template_arg = TemplateArguments(len(options), *template_arg)
|
||||
else:
|
||||
# Lambda
|
||||
arg = await cg.process_lambda(
|
||||
config[CONF_LAMBDA],
|
||||
[(global_ns.namespace("StringRef &").operator("const"), "current")] + args,
|
||||
return_type=cg.bool_,
|
||||
)
|
||||
template_arg = TemplateArguments(0, *template_arg)
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, arg)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"select.operation",
|
||||
SelectOperationAction,
|
||||
|
||||
@@ -66,4 +66,34 @@ template<typename... Ts> class SelectOperationAction : public Action<Ts...> {
|
||||
Select *select_;
|
||||
};
|
||||
|
||||
template<size_t N, typename... Ts> class SelectIsCondition : public Condition<Ts...> {
|
||||
public:
|
||||
SelectIsCondition(Select *parent, const char *const *option_list) : parent_(parent), option_list_(option_list) {}
|
||||
|
||||
bool check(const Ts &...x) override {
|
||||
auto current = this->parent_->current_option();
|
||||
for (size_t i = 0; i != N; i++) {
|
||||
if (current == this->option_list_[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected:
|
||||
Select *parent_;
|
||||
const char *const *option_list_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class SelectIsCondition<0, Ts...> : public Condition<Ts...> {
|
||||
public:
|
||||
SelectIsCondition(Select *parent, std::function<bool(const StringRef &, const Ts &...)> &&f)
|
||||
: parent_(parent), f_(f) {}
|
||||
|
||||
bool check(const Ts &...x) override { return this->f_(this->parent_->current_option(), x...); }
|
||||
|
||||
protected:
|
||||
Select *parent_;
|
||||
std::function<bool(const StringRef &, const Ts &...)> f_;
|
||||
};
|
||||
} // namespace esphome::select
|
||||
|
||||
@@ -124,8 +124,8 @@ void SEN5XComponent::setup() {
|
||||
sen5x_type = SEN55;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
|
||||
}
|
||||
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
|
||||
if (this->humidity_sensor_ && sen5x_type == SEN50) {
|
||||
ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55");
|
||||
this->humidity_sensor_ = nullptr; // mark as not used
|
||||
@@ -159,14 +159,28 @@ void SEN5XComponent::setup() {
|
||||
// This ensures the baseline storage is cleared after OTA
|
||||
// Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
|
||||
uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial);
|
||||
this->pref_ = global_preferences->make_preference<uint16_t[4]>(hash, true);
|
||||
this->voc_baseline_time_ = App.get_loop_component_start_time();
|
||||
if (this->pref_.load(&this->voc_baseline_state_)) {
|
||||
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) {
|
||||
ESP_LOGE(TAG, "VOC Baseline State write to sensor failed");
|
||||
} else {
|
||||
ESP_LOGV(TAG, "VOC Baseline State loaded");
|
||||
delay(20);
|
||||
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
|
||||
|
||||
if (this->pref_.load(&this->voc_baselines_storage_)) {
|
||||
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
|
||||
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
|
||||
}
|
||||
|
||||
// Initialize storage timestamp
|
||||
this->seconds_since_last_store_ = 0;
|
||||
|
||||
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
|
||||
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
|
||||
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
|
||||
uint16_t states[4];
|
||||
|
||||
states[0] = this->voc_baselines_storage_.state0 >> 16;
|
||||
states[1] = this->voc_baselines_storage_.state0 & 0xFFFF;
|
||||
states[2] = this->voc_baselines_storage_.state1 >> 16;
|
||||
states[3] = this->voc_baselines_storage_.state1 & 0xFFFF;
|
||||
|
||||
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
|
||||
ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,14 +288,6 @@ void SEN5XComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s",
|
||||
LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value())));
|
||||
}
|
||||
if (this->voc_sensor_) {
|
||||
char hex_buf[5 * 4];
|
||||
format_hex_pretty_to(hex_buf, this->voc_baseline_state_, 4, 0);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Store Baseline: %s\n"
|
||||
" State: %s\n",
|
||||
TRUEFALSE(this->store_baseline_), hex_buf);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
|
||||
@@ -298,6 +304,36 @@ void SEN5XComponent::update() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
|
||||
// much
|
||||
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
|
||||
if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
|
||||
// run it a bit later to avoid adding a delay here
|
||||
this->set_timeout(550, [this]() {
|
||||
uint16_t states[4];
|
||||
if (this->read_data(states, 4)) {
|
||||
uint32_t state0 = states[0] << 16 | states[1];
|
||||
uint32_t state1 = states[2] << 16 | states[3];
|
||||
if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
|
||||
MAXIMUM_STORAGE_DIFF ||
|
||||
(uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
|
||||
MAXIMUM_STORAGE_DIFF) {
|
||||
this->seconds_since_last_store_ = 0;
|
||||
this->voc_baselines_storage_.state0 = state0;
|
||||
this->voc_baselines_storage_.state1 = state1;
|
||||
|
||||
if (this->pref_.save(&this->voc_baselines_storage_)) {
|
||||
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
|
||||
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Could not store VOC baselines");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_);
|
||||
@@ -366,29 +402,7 @@ void SEN5XComponent::update() {
|
||||
if (this->nox_sensor_ != nullptr) {
|
||||
this->nox_sensor_->publish_state(nox);
|
||||
}
|
||||
|
||||
if (!this->voc_sensor_ || !this->store_baseline_ ||
|
||||
(App.get_loop_component_start_time() - this->voc_baseline_time_) < SHORTEST_BASELINE_STORE_INTERVAL) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->voc_baseline_time_ = App.get_loop_component_start_time();
|
||||
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
} else {
|
||||
this->set_timeout(20, [this]() {
|
||||
if (!this->read_data(this->voc_baseline_state_, 4)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
} else {
|
||||
if (this->pref_.save(&this->voc_baseline_state_)) {
|
||||
ESP_LOGD(TAG, "VOC Baseline State saved");
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this->status_clear_warning();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ enum RhtAccelerationMode : uint16_t {
|
||||
HIGH_ACCELERATION = 2,
|
||||
};
|
||||
|
||||
struct Sen5xBaselines {
|
||||
int32_t state0;
|
||||
int32_t state1;
|
||||
} PACKED; // NOLINT
|
||||
|
||||
struct GasTuning {
|
||||
uint16_t index_offset;
|
||||
uint16_t learning_time_offset_hours;
|
||||
@@ -39,9 +44,11 @@ struct TemperatureCompensation {
|
||||
uint16_t time_constant;
|
||||
};
|
||||
|
||||
// Shortest time interval of 2H (in milliseconds) for storing baseline values.
|
||||
// Shortest time interval of 3H for storing baseline values.
|
||||
// Prevents wear of the flash because of too many write operations
|
||||
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 2 * 60 * 60 * 1000;
|
||||
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
|
||||
// Store anyway if the baseline difference exceeds the max storage diff value
|
||||
static const uint32_t MAXIMUM_STORAGE_DIFF = 50;
|
||||
|
||||
class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
@@ -100,8 +107,7 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
|
||||
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
|
||||
bool write_temperature_compensation_(const TemperatureCompensation &compensation);
|
||||
|
||||
uint16_t voc_baseline_state_[4]{0};
|
||||
uint32_t voc_baseline_time_;
|
||||
uint32_t seconds_since_last_store_;
|
||||
uint16_t firmware_version_;
|
||||
ERRORCODE error_code_;
|
||||
uint8_t serial_number_[4];
|
||||
@@ -126,6 +132,7 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
|
||||
optional<TemperatureCompensation> temperature_compensation_;
|
||||
ESPPreferenceObject pref_;
|
||||
std::string product_name_;
|
||||
Sen5xBaselines voc_baselines_storage_;
|
||||
};
|
||||
|
||||
} // namespace sen5x
|
||||
|
||||
@@ -210,7 +210,6 @@ SENSOR_MAP = {
|
||||
SETTING_MAP = {
|
||||
CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval",
|
||||
CONF_ACCELERATION_MODE: "set_acceleration_mode",
|
||||
CONF_STORE_BASELINE: "set_store_baseline",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "slow_pwm_output.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace slow_pwm {
|
||||
@@ -21,9 +20,7 @@ void SlowPWMOutput::set_output_state_(bool new_state) {
|
||||
}
|
||||
if (new_state != current_state_) {
|
||||
if (this->pin_) {
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
this->pin_->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGV(TAG, "Switching output pin %s to %s", pin_summary, ONOFF(new_state));
|
||||
ESP_LOGV(TAG, "Switching output pin %s to %s", this->pin_->dump_summary().c_str(), ONOFF(new_state));
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Switching to %s", ONOFF(new_state));
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
|
||||
spi_ns = cg.esphome_ns.namespace("spi")
|
||||
@@ -449,13 +448,9 @@ def spi_device_schema(
|
||||
)
|
||||
|
||||
|
||||
async def register_spi_device(
|
||||
var: cg.Pvariable, config: ConfigType, write_only: bool = False
|
||||
) -> None:
|
||||
async def register_spi_device(var, config):
|
||||
parent = await cg.get_variable(config[CONF_SPI_ID])
|
||||
cg.add(var.set_spi_parent(parent))
|
||||
if write_only:
|
||||
cg.add(var.set_write_only(True))
|
||||
if cs_pin := config.get(CONF_CS_PIN):
|
||||
pin = await cg.gpio_pin_expression(cs_pin)
|
||||
cg.add(var.set_cs_pin(pin))
|
||||
|
||||
@@ -195,11 +195,8 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
config.post_cb = nullptr;
|
||||
if (this->bit_order_ == BIT_ORDER_LSB_FIRST)
|
||||
config.flags |= SPI_DEVICE_BIT_LSBFIRST;
|
||||
if (this->write_only_) {
|
||||
if (this->write_only_)
|
||||
config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY;
|
||||
ESP_LOGD(TAG, "SPI device with CS pin %d using half-duplex mode (write-only)",
|
||||
Utility::get_pin_no(this->cs_pin_));
|
||||
}
|
||||
esp_err_t const err = spi_bus_add_device(this->channel_, &config, &this->handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Add device failed - err %X", err);
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1306_base.setup_ssd1306(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1322_base.setup_ssd1322(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1325_base.setup_ssd1325(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1327_base.setup_ssd1327(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1331_base.setup_ssd1331(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1351_base.setup_ssd1351(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await st7567_base.setup_st7567(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -173,7 +173,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
sequence = []
|
||||
for seq in config[CONF_INIT_SEQUENCE]:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "st7701s.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -184,11 +183,8 @@ void ST7701S::dump_config() {
|
||||
LOG_PIN(" DE Pin: ", this->de_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
for (size_t i = 0; i != data_pin_count; i++) {
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
|
||||
}
|
||||
for (size_t i = 0; i != data_pin_count; i++)
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
|
||||
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000));
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ async def to_code(config):
|
||||
config[CONF_INVERT_COLORS],
|
||||
)
|
||||
await setup_st7735(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -177,7 +177,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
cg.add(var.set_model_str(config[CONF_MODEL]))
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
if CONF_LAMBDA in config:
|
||||
lambda_ = await cg.process_lambda(
|
||||
|
||||
@@ -14,7 +14,9 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
||||
void set_parent(Sun *parent) { parent_ = parent; }
|
||||
void set_elevation(double elevation) { elevation_ = elevation; }
|
||||
void set_sunrise(bool sunrise) { sunrise_ = sunrise; }
|
||||
void set_format(const std::string &format) { format_ = format; }
|
||||
void set_format(const char *format) { this->format_ = format; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_format(const std::string &format) = delete;
|
||||
|
||||
void update() override {
|
||||
optional<ESPTime> res;
|
||||
@@ -29,14 +31,14 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
||||
}
|
||||
|
||||
char buf[ESPTime::STRFTIME_BUFFER_SIZE];
|
||||
size_t len = res->strftime_to(buf, this->format_.c_str());
|
||||
size_t len = res->strftime_to(buf, this->format_);
|
||||
this->publish_state(buf, len);
|
||||
}
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
std::string format_{};
|
||||
const char *format_{nullptr};
|
||||
Sun *parent_;
|
||||
double elevation_;
|
||||
bool sunrise_;
|
||||
|
||||
@@ -118,8 +118,7 @@ async def to_code(config):
|
||||
var = await alarm_control_panel.new_alarm_control_panel(config)
|
||||
await cg.register_component(var, config)
|
||||
if CONF_CODES in config:
|
||||
for acode in config[CONF_CODES]:
|
||||
cg.add(var.add_code(acode))
|
||||
cg.add(var.set_codes(config[CONF_CODES]))
|
||||
if CONF_REQUIRES_CODE_TO_ARM in config:
|
||||
cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM]))
|
||||
|
||||
|
||||
@@ -206,7 +206,13 @@ bool TemplateAlarmControlPanel::is_code_valid_(optional<std::string> code) {
|
||||
if (!this->codes_.empty()) {
|
||||
if (code.has_value()) {
|
||||
ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str());
|
||||
return (std::count(this->codes_.begin(), this->codes_.end(), code.value()) == 1);
|
||||
// Use strcmp for const char* comparison
|
||||
const char *code_cstr = code.value().c_str();
|
||||
for (const char *stored_code : this->codes_) {
|
||||
if (strcmp(stored_code, code_cstr) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "No code provided");
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
@@ -86,11 +87,14 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED);
|
||||
#endif
|
||||
|
||||
/** add a code
|
||||
/** Set the codes (from initializer list).
|
||||
*
|
||||
* @param code The code
|
||||
* @param codes The list of valid codes
|
||||
*/
|
||||
void add_code(const std::string &code) { this->codes_.push_back(code); }
|
||||
void set_codes(std::initializer_list<const char *> codes) { this->codes_ = codes; }
|
||||
|
||||
// Deleted overload to catch incorrect std::string usage at compile time
|
||||
void set_codes(std::initializer_list<std::string> codes) = delete;
|
||||
|
||||
/** set requires a code to arm
|
||||
*
|
||||
@@ -155,8 +159,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
uint32_t pending_time_;
|
||||
// the time in trigger
|
||||
uint32_t trigger_time_;
|
||||
// a list of codes
|
||||
std::vector<std::string> codes_;
|
||||
// a list of codes (const char* pointers to string literals in flash)
|
||||
FixedVector<const char *> codes_;
|
||||
// requires a code to arm
|
||||
bool requires_code_to_arm_ = false;
|
||||
bool supports_arm_home_ = false;
|
||||
|
||||
@@ -8,16 +8,23 @@ static const char *const TAG = "template.text";
|
||||
void TemplateText::setup() {
|
||||
if (this->f_.has_value())
|
||||
return;
|
||||
std::string value = this->initial_value_;
|
||||
if (!this->pref_) {
|
||||
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
|
||||
} else {
|
||||
uint32_t key = this->get_preference_hash();
|
||||
key += this->traits.get_min_length() << 2;
|
||||
key += this->traits.get_max_length() << 4;
|
||||
key += fnv1_hash(this->traits.get_pattern_c_str()) << 6;
|
||||
this->pref_->setup(key, value);
|
||||
|
||||
if (this->pref_ == nullptr) {
|
||||
// No restore - use const char* directly, no heap allocation needed
|
||||
if (this->initial_value_ != nullptr && this->initial_value_[0] != '\0') {
|
||||
ESP_LOGD(TAG, "State from initial: %s", this->initial_value_);
|
||||
this->publish_state(this->initial_value_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Need std::string for pref_->setup() to fill from flash
|
||||
std::string value{this->initial_value_ != nullptr ? this->initial_value_ : ""};
|
||||
uint32_t key = this->get_preference_hash();
|
||||
key += this->traits.get_min_length() << 2;
|
||||
key += this->traits.get_max_length() << 4;
|
||||
key += fnv1_hash(this->traits.get_pattern_c_str()) << 6;
|
||||
this->pref_->setup(key, value);
|
||||
if (!value.empty())
|
||||
this->publish_state(value);
|
||||
}
|
||||
|
||||
@@ -70,13 +70,15 @@ class TemplateText final : public text::Text, public PollingComponent {
|
||||
|
||||
Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; }
|
||||
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
void set_initial_value(const std::string &initial_value) { this->initial_value_ = initial_value; }
|
||||
void set_initial_value(const char *initial_value) { this->initial_value_ = initial_value; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_initial_value(const std::string &initial_value) = delete;
|
||||
void set_value_saver(TemplateTextSaverBase *restore_value_saver) { this->pref_ = restore_value_saver; }
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
bool optimistic_ = false;
|
||||
std::string initial_value_;
|
||||
const char *initial_value_{nullptr};
|
||||
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
|
||||
TemplateLambda<std::string> f_{};
|
||||
|
||||
|
||||
@@ -40,9 +40,6 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
// Unsigned subtraction handles wraparound correctly, then cast to signed
|
||||
int32_t diff = static_cast<int32_t>(epoch - static_cast<uint32_t>(current_time));
|
||||
if (diff >= -1 && diff <= 1) {
|
||||
// Time is already synchronized, but still call callbacks so components
|
||||
// waiting for time sync (e.g., uptime timestamp sensor) can initialize
|
||||
this->time_sync_callback_.call();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,7 @@ async def to_code(config):
|
||||
cg.add(var.set_broadcast_port(conf_port[CONF_BROADCAST_PORT]))
|
||||
if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255":
|
||||
cg.add(var.set_listen_address(listen_address))
|
||||
for address in config[CONF_ADDRESSES]:
|
||||
cg.add(var.add_address(str(address)))
|
||||
cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]]))
|
||||
if on_receive := config.get(CONF_ON_RECEIVE):
|
||||
on_receive = on_receive[0]
|
||||
trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "udp_component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace udp {
|
||||
namespace esphome::udp {
|
||||
|
||||
static const char *const TAG = "udp";
|
||||
|
||||
@@ -95,7 +94,7 @@ void UDPComponent::setup() {
|
||||
// 8266 and RP2040 `Duino
|
||||
for (const auto &address : this->addresses_) {
|
||||
auto ipaddr = IPAddress();
|
||||
ipaddr.fromString(address.c_str());
|
||||
ipaddr.fromString(address);
|
||||
this->ipaddrs_.push_back(ipaddr);
|
||||
}
|
||||
if (this->should_listen_)
|
||||
@@ -130,8 +129,8 @@ void UDPComponent::dump_config() {
|
||||
" Listen Port: %u\n"
|
||||
" Broadcast Port: %u",
|
||||
this->listen_port_, this->broadcast_port_);
|
||||
for (const auto &address : this->addresses_)
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", address.c_str());
|
||||
for (const char *address : this->addresses_)
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", address);
|
||||
if (this->listen_address_.has_value()) {
|
||||
char addr_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf));
|
||||
@@ -162,7 +161,6 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace udp
|
||||
} // namespace esphome
|
||||
} // namespace esphome::udp
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NETWORK
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
#include "esphome/components/socket/socket.h"
|
||||
@@ -9,15 +10,17 @@
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
#include <WiFiUdp.h>
|
||||
#endif
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace udp {
|
||||
namespace esphome::udp {
|
||||
|
||||
static const size_t MAX_PACKET_SIZE = 508;
|
||||
class UDPComponent : public Component {
|
||||
public:
|
||||
void add_address(const char *addr) { this->addresses_.emplace_back(addr); }
|
||||
void set_addresses(std::initializer_list<const char *> addresses) { this->addresses_ = addresses; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_addresses(std::initializer_list<std::string> addresses) = delete;
|
||||
void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); }
|
||||
void set_listen_port(uint16_t port) { this->listen_port_ = port; }
|
||||
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
||||
@@ -49,11 +52,10 @@ class UDPComponent : public Component {
|
||||
std::vector<IPAddress> ipaddrs_{};
|
||||
WiFiUDP udp_client_{};
|
||||
#endif
|
||||
std::vector<std::string> addresses_{};
|
||||
FixedVector<const char *> addresses_{};
|
||||
|
||||
optional<network::IPAddress> listen_address_{};
|
||||
};
|
||||
|
||||
} // namespace udp
|
||||
} // namespace esphome
|
||||
} // namespace esphome::udp
|
||||
#endif
|
||||
|
||||
@@ -239,7 +239,7 @@ async def to_code(config):
|
||||
raise NotImplementedError()
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user