Merge remote-tracking branch 'origin/dev' into esp8266-entity-names-flash

# Conflicts:
#	esphome/components/uart/button/uart_button.cpp
#	esphome/components/uart/switch/uart_switch.cpp
This commit is contained in:
J. Nick Koston
2026-10-11 15:36:07 -10:00
448 changed files with 14537 additions and 2179 deletions
+26 -10
View File
@@ -42,8 +42,8 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo,
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
message +=
`Hey @${prAuthor}, thanks for the contribution! Just a heads up, ` +
`this PR is on the large side `;
`Hey @${prAuthor}, thanks for the contribution. This PR is on the ` +
`large side `;
if (tooManyLabels && tooManyChanges) {
message +=
@@ -57,16 +57,31 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo,
}
message += `, which makes it harder for maintainers to review.\n\n`;
let condition;
if (tooManyLabels && tooManyChanges) {
condition =
`is at most ${TOO_BIG_THRESHOLD} lines (excluding tests) and touches ` +
`at most ${MAX_LABELS} components/areas`;
} else if (tooManyLabels) {
condition = `touches at most ${MAX_LABELS} components/areas`;
} else {
condition = `is at most ${TOO_BIG_THRESHOLD} lines (excluding tests)`;
}
message +=
`Smaller, focused PRs tend to be reviewed much faster since they ` +
`fit into the short gaps between other maintainer work; large ones ` +
`often have to wait for a rare long uninterrupted block of time. ` +
`If you can break this up into smaller pieces that can be reviewed ` +
`independently, it will almost certainly land faster overall.\n\n`;
`**This PR will not be reviewed until it ${condition}.** We know ` +
`that is not what you want to hear after this much work, and we want ` +
`to be upfront so you do not wait for a review that is not coming: ` +
`please split the change. The label is removed automatically once ` +
`the PR ${condition}.\n\n`;
message +=
`Before putting more time in, it's also worth popping into ` +
`\`#devs\` on [Discord](https://esphome.io/chat) so we can help ` +
`you scope things and flag anything already in flight.\n\n`;
`There is no exception you can request. Rarely, a maintainer agrees in ` +
`\`#devs\` on [Discord](https://esphome.io/chat) to review a change that ` +
`genuinely cannot be split as one piece and adds the \`mega-pr\` label, ` +
`but that is rare enough that you should plan on splitting rather than ` +
`wait for it. Smaller, focused PRs are picked up much faster because ` +
`they fit into the short gaps between other maintainer work. For a new ` +
`component, the first PR brings the hub and a single platform (for ` +
`example \`sensor\`); each further platform is its own PR on top of it.\n\n`;
message +=
`For more details (including how to split the work up), see: ` +
`https://developers.esphome.io/contributing/submitting-your-work/` +
@@ -214,6 +229,7 @@ async function handleMaintainerAccessComment(github, context, maintainerAccess)
}
module.exports = {
generateReviewMessages,
handleReviews,
handleMaintainerAccessComment
};
@@ -0,0 +1,38 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { generateReviewMessages } = require('../reviews');
const MAX_LABELS = 15;
const TOO_BIG_THRESHOLD = 1000;
function tooBigMessage(labelCount, additions) {
const files = [{ filename: 'esphome/components/a/a.cpp', additions, deletions: 0 }];
const messages = generateReviewMessages(['too-big'], labelCount, null, files, additions, 0, 'someone', MAX_LABELS, TOO_BIG_THRESHOLD);
assert.equal(messages.length, 1);
return messages[0];
}
describe('too-big review message', () => {
it('names the line cap when the PR is too long', () => {
const message = tooBigMessage(3, 1500);
assert.match(message, /will not be reviewed until it is at most 1000 lines \(excluding tests\)\./);
assert.match(message, /removed automatically once the PR is at most 1000 lines/);
assert.doesNotMatch(message, /components\/areas\./);
});
it('names the area cap when the PR touches too many areas but is short', () => {
const message = tooBigMessage(20, 200);
assert.match(message, /will not be reviewed until it touches at most 15 components\/areas\./);
assert.doesNotMatch(message, /until it is at most 1000 lines/);
});
it('names both caps when both are exceeded', () => {
const message = tooBigMessage(20, 1500);
assert.match(message, /is at most 1000 lines \(excluding tests\) and touches at most 15 components\/areas\./);
});
it('tells new-component authors to bring one platform first', () => {
assert.match(tooBigMessage(3, 1500), /hub and a single platform/);
assert.match(tooBigMessage(3, 1500), /mega-pr/);
});
});
+86
View File
@@ -1335,6 +1335,91 @@ jobs:
# config first, so a separate config pass is redundant.
python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests
test-esp8266-native-windows:
name: Compile esp8266 with the native toolchain on Windows (ccache ${{ matrix.ccache }})
runs-on: windows-latest
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.esp8266-native == 'true'
# A hung compiler must fail the job, not hold the runner for six hours.
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
# Both ways the build runs on Windows: ESPHome Desktop bundles
# ccache and puts it on PATH, a pip install has none.
ccache: ["1", "0"]
env:
ESPHOME_CCACHE_ENABLE: ${{ matrix.ccache }}
# The test script prints characters the cp1252 console cannot encode
PYTHONUTF8: "1"
# The exact static build ESPHome Desktop ships
CCACHE_URL: https://github.com/ccache/ccache/releases/download/v4.14/ccache-4.14-windows-x86_64.zip
CCACHE_SHA256: 2568347a697e103ca1b073981c704ad76fb2507d066c38dba038dd73399d968f
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Install ccache
if: matrix.ccache == '1'
shell: pwsh
run: |
$dir = Join-Path $env:RUNNER_TEMP "ccache"
New-Item -ItemType Directory -Force $dir | Out-Null
$zip = Join-Path $dir "ccache.zip"
Invoke-WebRequest -Uri $env:CCACHE_URL -OutFile $zip
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
if ($hash -ne $env:CCACHE_SHA256) {
Write-Host "::error::ccache archive sha256 $hash does not match $env:CCACHE_SHA256"
exit 1
}
Expand-Archive -Path $zip -DestinationPath $dir
$bin = Get-ChildItem -Path $dir -Recurse -Filter ccache.exe | Select-Object -First 1
Add-Content -Path $env:GITHUB_PATH -Value $bin.DirectoryName
& $bin.FullName --version
- name: Compile
shell: pwsh
# One component test: web_server brings wifi, api and the async web
# server libraries, the shape of a typical ESP8266 device
run: |
. .\venv\Scripts\Activate.ps1
$arguments = "script/test_build_components.py", "-e", "compile", "-t", "esp8266-ard", "-c", "web_server", "--base-only", "--toolchain", "arduino", "--fail-on-no-tests"
$p = Start-Process -FilePath python -ArgumentList $arguments -NoNewWindow -PassThru
# Reading the handle keeps ExitCode available after the process exits
$null = $p.Handle
if (-not $p.WaitForExit(25 * 60 * 1000)) {
Write-Host "::error::The compile did not finish in 25 minutes; processes still running:"
Get-CimInstance Win32_Process |
Where-Object { $_.CommandLine -match 'xtensa|ccache|ninja|esphome' } |
Select-Object ProcessId, ParentProcessId, CreationDate, CommandLine |
Format-List | Out-String -Width 4096 | Write-Host
Get-Process |
Where-Object { $_.Name -match 'cc1|ccache|ninja|xtensa|python' } |
Select-Object Id, Name, CPU, WorkingSet64 |
Format-Table | Out-String | Write-Host
taskkill /PID $p.Id /T /F
exit 1
}
exit $p.ExitCode
- name: Show ccache statistics
# Also after a failed or killed compile: the counters say whether
# ccache was ever reached
if: always() && matrix.ccache == '1'
shell: pwsh
# The build keeps its store under the esphome tools cache
run: |
$env:CCACHE_DIR = Join-Path $env:LOCALAPPDATA "esphome\Cache\arduino8266\ccache"
ccache -s
device-builder:
name: Test downstream esphome/device-builder
runs-on: ubuntu-24.04
@@ -1705,6 +1790,7 @@ jobs:
- test-build-components-split
- test-esp32-platformio
- test-esp8266-native
- test-esp8266-native-windows
- device-builder
- memory-impact-target-branch
- memory-impact-pr-branch
+15 -1
View File
@@ -16,16 +16,18 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # main
uses: esphome/workflows/.github/workflows/stale.yml@4fbce13026c393d6eb6b247e394dcd9ef1af76c2 # main
secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with:
# Live only on dev: a workflow_dispatch from any other branch is a dry run
dry-run: ${{ github.ref != 'refs/heads/dev' }}
days-before-stale: 90
days-before-stale-conflicting: 30
days-before-close: 7
stale-label: stale
exempt-label: not-stale
conflict-label: needs-rebase
ignored-users: esphbot,codecov-commenter
stale-pr-message: >
There hasn't been any activity on this pull request recently. This
@@ -36,6 +38,18 @@ jobs:
to keep it open. Also, please merge the latest dev branch into your
branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!
conflict-pr-message: >
This pull request has merge conflicts. Please merge the latest dev
branch into your branch to resolve them.
stale-conflicting-pr-message: >
The merge conflicts on this pull request have not been resolved for
30 days, so it has been marked as stale and will be closed if they
are not resolved within 7 days.
Please merge the latest dev branch into your branch. Once the pull
request is mergeable again the stale label is removed.
Thank you for your contribution!
stale-issue-message: >
There hasn't been any activity on this issue recently. Due to the
+24
View File
@@ -67,6 +67,7 @@ file does, and it is the authority when they disagree. The most useful starting
- Function-local constants: `lower_snake_case`
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
- Favor descriptive names over abbreviations
- Device components are named brand plus model (`siemens_ira211`), without a protocol-family prefix
- Enumerator names: prefix every value of an `enum class` with the enum name converted to
`UPPER_SNAKE_CASE` (e.g. `UARTFlushResult::UART_FLUSH_RESULT_SUCCESS`). Never use bare
names like `SUCCESS`, `FAILURE`, `OK`, or `FAIL`: platform SDK headers define macros with
@@ -177,6 +178,8 @@ file does, and it is the authority when they disagree. The most useful starting
Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use
`set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms:
https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb
* **Repeating work:** use `set_interval`, never a `set_timeout` that re-arms itself; every re-arm
rebuilds the callback and re-queues a scheduler item, and the period drifts.
* **Scheduler ids:** name a timer only when it must be cancelled or replaced, and use a
`static constexpr uint32_t` id, never a string. Ids are per component instance and cannot clash with other
components, so number them from 0 and keep all of a component's ids together in one place.
@@ -191,6 +194,18 @@ file does, and it is the authority when they disagree. The most useful starting
// Bad - this is exactly what the base class already does
float get_setup_priority() const override { return setup_priority::DATA; }
```
* **Constant data lives in RAM on ESP8266** unless marked `PROGMEM`: `const` tables, class-scope
constant arrays, and in-class `std::array` initializers (a `.rodata` template plus a copy). Mark the
table `PROGMEM` and read it with `progmem_memcpy()` or `progmem_read_byte()` (`esphome/core/hal.h`);
for a table of structs copy the whole entry out and return the copy, never a pointer into flash.
On other platforms they are a plain `memcpy` and dereference, so no `#ifdef` is needed. Declare log tags with
`ESPHOME_LOG_TAG(TAG, "name")`, pass status text as `LOG_STR`, e.g.
`this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL))`, and give an action whose payload is a byte array
a `TEMPLATABLE_BYTES` field so a static code stays in flash.
* **Failures after `setup()`:** `mark_failed()` is for `setup()` only. A runtime read or write failure
sets `status_set_warning()` and the next success clears it; writes skip work when `is_failed()`.
* **Fixed platform facts compile to nothing:** gate on `#ifdef USE_ESP8266` and friends, not on a
runtime check of something the build already knows.
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
can be stored in flash rather than RAM.
```cpp
@@ -534,6 +549,7 @@ file does, and it is the authority when they disagree. The most useful starting
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
An intentionally empty body (an unused `register_dumper` builder, for example) gets a one-line docstring, not `pass`.
* **Configuration Validation:**
* **Reuse existing validators:** Before writing a custom validator, check for an existing one in `config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components. For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`.
@@ -569,6 +585,13 @@ file does, and it is the authority when they disagree. The most useful starting
* **Testing:**
* **Python:** Run unit tests with `pytest`.
* **C++:** Use `clang-tidy` for static analysis.
* **C++ unit tests:** gtests in `tests/components/<component>/*.cpp`, run with
`script/cpp_unit_test.py <component>`; see `tests/components/README.md`. Set the loop clock in a test
with `LoopBlockingGuard{nullptr, nullptr, now_ms}`; the runner resets it before every test.
* **Integration tests:** `tests/integration/` runs real firmware on the host. Serial components prove
timeouts and recovery with the `uart_mock` fixture (`tests/integration/test_uart_mock_*.py`).
* **Keep build-test variants few:** one `test.<platform>.yaml` per platform the code actually differs
on. A variant per board for platform-identical code only adds CI time.
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
```
tests/
@@ -615,6 +638,7 @@ file does, and it is the authority when they disagree. The most useful starting
name: My Sensor
```
Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time.
`!extend` and `!remove` in a `test.*.yaml` also make the component ungroupable; add a standalone entity with its own id instead.
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
```bash
+1 -1
View File
@@ -570,7 +570,7 @@ esphome/components/tca9555/* @mobrembski
esphome/components/tcl112/* @glmnet
esphome/components/tcp_uart/* @Bascht74
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax
esphome/components/teleinfo/* @0hax @FredM67
esphome/components/tem3200/* @bakerkj
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/template/datetime/* @rfdarter
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.1
RUN \
platformio settings set enable_telemetry No \
+34 -2
View File
@@ -51,6 +51,7 @@ from esphome.const import (
KEY_ESP32,
KEY_VARIANT,
SECRETS_FILES,
ErrorFormat,
Toolchain,
)
from esphome.core import CORE, EsphomeError, coroutine
@@ -2205,6 +2206,16 @@ def _add_states_args(parser: argparse.ArgumentParser) -> None:
)
def _error_format(value: str) -> ErrorFormat:
try:
return ErrorFormat(value.lower())
except ValueError:
choices = ", ".join(ErrorFormat)
raise argparse.ArgumentTypeError(
f"invalid choice: '{value}' (choose from {choices})"
) from None
def parse_args(argv):
options_parser = argparse.ArgumentParser(add_help=False)
options_parser.add_argument(
@@ -2267,6 +2278,17 @@ def parse_args(argv):
),
)
options_parser.add_argument(
"--error-format",
type=_error_format,
metavar="{" + ",".join(f.value for f in ErrorFormat) + "}",
help=(
"How configuration errors are shown. 'yaml' shows them inside the "
"failing YAML; 'line' prints one 'file:line:column: error: message' "
"line per error. Default: $ESPHOME_ERROR_FORMAT, else 'yaml'."
),
)
parser = argparse.ArgumentParser(
description=f"ESPHome {const.__version__}", parents=[options_parser]
)
@@ -2570,9 +2592,18 @@ def parse_args(argv):
args, unknown_args = parser.parse_known_args(arguments)
if unknown_args:
_LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
return args
else:
args = parser.parse_args(arguments)
return parser.parse_args(arguments)
# Read here rather than as the option default, so a bad value is reported
# against the environment variable instead of --error-format
if args.error_format is None:
env_value = os.getenv("ESPHOME_ERROR_FORMAT", ErrorFormat.YAML.value)
try:
args.error_format = _error_format(env_value)
except argparse.ArgumentTypeError as err:
parser.error(f"ESPHOME_ERROR_FORMAT: {err}")
return args
def _warn_if_source_tree_mismatch() -> None:
@@ -2645,6 +2676,7 @@ def run_esphome(argv):
args = parse_args(argv)
CORE.dashboard = args.dashboard
CORE.testing_mode = args.testing_mode
CORE.error_format = args.error_format
# Create address cache from command-line arguments
CORE.address_cache = AddressCache.from_cli_args(
+79 -15
View File
@@ -724,7 +724,7 @@ async def condition_group_to_code(
if len(conditions) == 1:
return conditions[0]
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
condition_id, cg.TemplateArguments(len(conditions), *template_arg), *conditions
)
@@ -840,11 +840,13 @@ async def if_action_to_code(
condition = await build_condition(config[cond_conf], template_arg, args)
var = cg.new_Pvariable(action_id, if_template_arg, condition)
if CONF_THEN in config:
actions = await build_action_list(config[CONF_THEN], template_arg, args)
cg.add(var.add_then(actions))
for action in await build_action_list(config[CONF_THEN], template_arg, args):
cg.add(var.add_then_action(action))
cg.add(var.finish_then())
if has_else:
actions = await build_action_list(config[CONF_ELSE], template_arg, args)
cg.add(var.add_else(actions))
for action in await build_action_list(config[CONF_ELSE], template_arg, args):
cg.add(var.add_else_action(action))
cg.add(var.finish_else())
return var
@@ -867,8 +869,9 @@ async def while_action_to_code(
) -> MockObj:
condition = await build_condition(config[CONF_CONDITION], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, condition)
actions = await build_action_list(config[CONF_THEN], template_arg, args)
cg.add(var.add_then(actions))
for action in await build_action_list(config[CONF_THEN], template_arg, args):
cg.add(var.add_then_action(action))
cg.add(var.finish_then())
return var
@@ -897,7 +900,9 @@ async def repeat_action_to_code(
cg.TemplateArguments(cg.uint32, *template_arg.args),
[(cg.uint32, "iteration"), *args],
)
cg.add(var.add_then(actions))
for action in actions:
cg.add(var.add_then_action(action))
cg.add(var.finish_then())
return var
@@ -1033,21 +1038,79 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool:
Non-synchronous actions (delay, wait_until, script.wait, etc.) store
trigger args for later execution, making non-owning types like StringRef
unsafe.
Accepts an action list, a single action, or an automation. Only the keys of
action entries are looked up as action names; the options of an action are
only searched for nested action lists (``then:``, ``else:``, ...), so an
option that shares a name with an action, such as the ``delay:`` option of
``speaker_source.set_playlist_delay``, is not mistaken for that action.
"""
if isinstance(actions, list):
return any(has_non_synchronous_actions(item) for item in actions)
if isinstance(actions, dict):
for key in actions:
for key, options in actions.items():
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
return True
return any(
has_non_synchronous_actions(v)
for v in actions.values()
if isinstance(v, (list, dict))
)
if _options_have_non_synchronous_actions(options):
return True
return False
def _options_have_non_synchronous_actions(options: Any) -> bool:
"""Search an action's options for nested action lists, without treating option keys as actions."""
if isinstance(options, list):
return has_non_synchronous_actions(options)
if isinstance(options, dict):
return any(_options_have_non_synchronous_actions(v) for v in options.values())
return False
def validate_no_deferring_actions(
context: str, data: str = "the trigger data"
) -> Callable[[ConfigType], ConfigType]:
"""Return a validator that rejects deferring actions in a validated automation.
Use it for triggers whose arguments only live while the callback runs (views into a reused
buffer, a JSON document freed on return, ...): deferring actions keep the arguments for later.
:param context: Where the automation is, for the error message, e.g. ``"on_json and on_data"``.
:param data: What is only valid while the automation runs, for the error message.
"""
def validator(value: ConfigType) -> ConfigType:
if has_non_synchronous_actions(value):
raise cv.Invalid(
f"Deferring actions (delay, wait_until, script.wait, ...) are not allowed in "
f"{context}: {data} is only valid while the automation runs. Copy what you need "
"into globals first, then defer in a separate script or automation."
)
return value
return validator
def validate_synchronous_automation(
context: str,
data: str = "the trigger data",
extra_schema=None,
extra_validators=None,
single: bool = False,
) -> cv.All:
"""``validate_automation()`` that also rejects deferring actions.
See ``validate_no_deferring_actions()`` for ``context`` and ``data``; the other parameters are
passed to ``validate_automation()``.
"""
return cv.All(
validate_automation(
extra_schema=extra_schema,
extra_validators=extra_validators,
single=single,
),
validate_no_deferring_actions(context, data),
)
async def _new_automation(
args: TemplateArgsType, config: ConfigType, *ctor_args: MockObj
) -> MockObj:
@@ -1055,7 +1118,8 @@ async def _new_automation(
templ = cg.TemplateArguments(*(arg[0] for arg in args))
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, *ctor_args)
actions = await build_action_list(config[CONF_THEN], templ, args)
cg.add(obj.add_actions(actions))
for action in actions:
cg.add(obj.add_action(action))
return obj
+1 -1
View File
@@ -1057,7 +1057,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
cxx = (toolchain_tool(paths.toolchain, "g++"),)
lines = [
*tool_lines((toolchain_tool(paths.toolchain, "gcc"),), cxx, ccache),
*compile_rule_lines(),
*compile_rule_lines(ccache),
*ar_rule_lines(toolchain_tool(paths.toolchain, "ar")),
*pch_rule_lines(),
"rule link",
+1 -1
View File
@@ -240,7 +240,7 @@ def write_project(compilers: HostCompilers, ccache: str | None) -> bool:
lines = [
*tool_lines(compilers.cc, compilers.cxx, ccache),
*compile_rule_lines(),
*compile_rule_lines(ccache),
*pch_rule_lines(),
"rule link",
" command = $cxx -o $out $linkflags @$out.rsp $archives $libdirflags $libflags",
+14 -7
View File
@@ -74,33 +74,40 @@ def tool_lines(
# path gets
f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
f"buildtool = {_q(_BUILD_TOOL)}",
f"ccache = {_q(ccache) if ccache else ''}",
*([f"ccache = {_q(ccache)}"] if ccache else []),
"",
]
def compile_rule_lines() -> list[str]:
"""The compile rules; ``$own_includes`` is empty unless an edge sets it."""
def compile_rule_lines(ccache: str | None) -> list[str]:
"""The compile rules; ``$own_includes`` is empty unless an edge sets it.
``ccache`` is the launcher ``tool_lines`` was given; the same test here
keeps the variable and the rules in step. It is spliced in rather than
left as an empty variable: a leading space on the command line makes
CreateProcess fail on Windows.
"""
launcher = "$ccache " if ccache else ""
return [
"rule c",
" command = $ccache $cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
f" command = {launcher}$cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CC $out",
"rule cxx",
" command = $ccache $cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
f" command = {launcher}$cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CXX $out",
"rule aspp",
" command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
f" command = {launcher}$cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = AS $out",
# Plain assembler, as SCons's ASCOM: no preprocessor, so no
# depfile and no $flags (defines/includes) either
"rule asm",
" command = $ccache $cc -x assembler $asflags -c $in -o $out",
f" command = {launcher}$cc -x assembler $asflags -c $in -o $out",
" description = AS $out",
]
+1
View File
@@ -44,6 +44,7 @@ from esphome.cpp_generator import ( # noqa: F401
safe_exp,
set_cpp_standard,
shared_progmem_array,
shared_progmem_string,
statement,
static_const_array,
static_function,
+8 -34
View File
@@ -54,7 +54,7 @@ from esphome.const import (
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.cpp_generator import Expression, MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.helpers import cpp_string_escape, fnv1_hash
from esphome.helpers import fnv1_hash
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigFragmentType, ConfigType
@@ -537,28 +537,11 @@ FINAL_VALIDATE_SCHEMA = cv.All(
)
def _add_action_strings(
index: int, strings: list[str | None], interned: dict[str, MockObj]
) -> MockObj:
"""Emit the PROGMEM string table for one action.
Each string is its own PROGMEM array because on ESP8266 .rodata is RAM, and identical
strings are shared between actions through `interned`.
"""
entries: list[MockObj] = []
for string in strings:
if string is None:
entries.append(cg.nullptr)
continue
if (var := interned.get(string)) is None:
var = interned[string] = cg.progmem_array(
ID(f"api_action_str{len(interned)}", is_declaration=True, type=cg.char),
string,
)
entries.append(var)
def _add_action_strings(index: int, strings: list[str | None]) -> MockObj:
"""Emit the PROGMEM string table for one action."""
return cg.progmem_array(
ID(f"api_action{index}_strings", is_declaration=True, type=cg.const_char_ptr),
entries,
[cg.nullptr if s is None else cg.shared_progmem_string(s) for s in strings],
)
@@ -611,7 +594,6 @@ async def to_code(config: ConfigType) -> None:
has_metadata = _has_action_metadata(actions)
if has_metadata:
cg.add_define("USE_API_USER_DEFINED_ACTION_METADATA")
interned_strings: dict[str, MockObj] = {}
# Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.MockObj] = []
for index, conf in enumerate(actions):
@@ -656,7 +638,7 @@ async def to_code(config: ConfigType) -> None:
service_template_args.append(native)
func_args.append((native, name))
strings = _action_strings(conf, has_metadata)
table = _add_action_strings(index, strings, interned_strings)
table = _add_action_strings(index, strings)
if CORE.is_esp8266:
scratch_size = max(scratch_size, _action_strings_size(strings))
# Template args: supports_response mode, then user service arg types
@@ -682,7 +664,7 @@ async def to_code(config: ConfigType) -> None:
unregister_id,
var,
)
cg.add(auto.add_actions([unregister_action]))
cg.add(auto.add_action(unregister_action))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CORE.is_esp8266 and has_user_actions:
@@ -828,14 +810,6 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
)
def _field_string(value: str) -> Expression:
# ESP8266 can only keep a string in flash as its own PROGMEM array
literal = cg.RawExpression(cpp_string_escape(value))
if CORE.is_esp8266:
return cg.shared_progmem_array("ha_field_str", cg.char, literal)
return literal
async def _new_service_call_action(
server_id: ID,
action_id: ID,
@@ -858,12 +832,12 @@ async def _new_service_call_action(
)
async def field(key: str | None, value: Any, output_type: Any = None) -> Expression:
key_exp = cg.nullptr if key is None else _field_string(key)
key_exp = cg.nullptr if key is None else cg.shared_progmem_string(key)
if cg.is_template(value):
# output_type=None lets lambdas return numbers or char pointers; C++ converts them
lam = await cg.process_lambda(value, args, return_type=output_type)
return cg.RawExpression(f"{field_type}::from_lambda({key_exp}, {lam})")
return cg.ArrayInitializer(key_exp, _field_string(value), cg.nullptr)
return cg.ArrayInitializer(key_exp, cg.shared_progmem_string(value), cg.nullptr)
entries = [await field(None, service, cg.std_string)]
for group in groups:
+18 -5
View File
@@ -444,7 +444,7 @@ message DeviceWizardRequest {
// {"version": 1,
// "pages": [{"title": "...", "description": "...",
// "entities": [{"key": 123, "device_id": 456, "description": "..."}],
// "inputs": [{"key": 789, "description": "...",
// "inputs": [{"key": 789, "description": "...", "multiple": true, "max_length": 1023,
// "entity_filters": [{"integration": "...", "domain": ["..."],
// "device_class": ["..."], "supported_features": ["..."]}]}]}]}
//
@@ -468,6 +468,14 @@ message DeviceWizardRequest {
// an entity matches a filter when it satisfies every field that is set, and an entity
// that matches any one of the filters is accepted.
//
// An input with "multiple": true lets the user pick several entities. The client sends
// them in one WizardInputSetRequest as a comma separated list with no spaces
// ("calendar.a,calendar.b"); the device also accepts spaces around each id and stores the
// list without them. "max_length" (only on a multiple input, 255 when left out) is the
// longest list in bytes the device accepts. The version stays 1: a client that does not
// know "multiple" ignores it, shows a single picker and sends one entity id, which a
// multiple input accepts.
//
// The device keeps the entity id of an input in RAM only and does not store it across
// restarts. The client therefore stores the choice itself and, after every connection
// and before it sends SubscribeHomeAssistantStatesRequest, sends a WizardInputSetRequest
@@ -483,8 +491,13 @@ message DeviceWizardResponse {
// Sets the entity id of a wizard input (see WizardInputField.key). Clients send
// this only when DeviceCapabilitiesResponse.wizard.configured is set. There is no
// reply. The device ignores a request whose key matches no input or whose entity_id
// is empty, longer than 255 bytes or has no '.'. Otherwise it keeps the id in RAM,
// reply. The device ignores a request whose key matches no input. A single input is
// ignored when the entity_id is empty, longer than 255 bytes or has no '.'. A multiple
// input takes a comma separated list ("calendar.a,calendar.b"); the client sends no
// spaces, the device trims spaces around each id and stores the list without them. The
// whole request is ignored when any id is empty or has no '.', or when the list without
// spaces does not fit the input's max_length (255 by default). Otherwise it keeps the id
// or list in RAM,
// not across restarts, and when the input belongs to a homeassistant platform entity
// it subscribes to that entity's state. Clients send this for every input right
// after connecting, before SubscribeHomeAssistantStatesRequest. A request that
@@ -496,7 +509,7 @@ message WizardInputSetRequest {
option (ifdef) = "USE_API_WIZARD_INPUTS";
fixed32 key = 1;
string entity_id = 2 [(max_data_length) = 255];
string entity_id = 2 [(max_data_length) = 4095];
}
message ListEntitiesRequest {
@@ -2557,7 +2570,7 @@ message ListEntitiesEventResponse {
EntityCategory entity_category = 7;
string device_class = 8 [(max_data_length) = 47];
repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"];
repeated string event_types = 9 [(container_pointer_no_template) = "ConstVector<const char *, true>"];
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
message EventResponse {
+1 -1
View File
@@ -3460,7 +3460,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("list_entities_event_response"); }
#endif
StringRef device_class{nullptr, 0}; // null until set, encode only
const FixedVector<const char *> *event_types{};
const ConstVector<const char *, true> *event_types{};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
+49 -7
View File
@@ -14,26 +14,68 @@ namespace esphome::api {
ESPHOME_LOG_TAG(TAG, "api.wizard");
#ifdef USE_API_WIZARD_INPUTS
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
// Walks the comma separated entity ids in value, without the spaces around each. Returns the length they take joined
// by ',', or 0 when one is empty or has no '.'. Writes the joined list to out unless it is nullptr.
static size_t wizard_join_entity_ids(const char *value, size_t length, char *out) {
size_t joined = 0;
const char *end = value + length;
while (true) {
const char *comma = static_cast<const char *>(memchr(value, ',', end - value));
const char *id_end = comma != nullptr ? comma : end;
while (value < id_end && *value == ' ')
value++;
while (id_end > value && id_end[-1] == ' ')
id_end--;
size_t id_length = id_end - value;
if (id_length == 0 || memchr(value, '.', id_length) == nullptr)
return 0;
if (joined != 0) {
if (out != nullptr)
out[joined] = ',';
joined++;
}
if (out != nullptr)
memcpy(out + joined, value, id_length);
joined += id_length;
if (comma == nullptr)
return joined;
value = comma + 1;
}
}
#endif // USE_API_WIZARD_MULTIPLE_INPUTS
static bool wizard_entity_id_valid(const char *entity_id, size_t length) {
return length > 0 && length < WIZARD_ENTITY_ID_BUFFER_SIZE && memchr(entity_id, '.', length) != nullptr;
}
const char *wizard_set_input(const WizardInputSetRequest &msg) {
if (!wizard_entity_id_valid(msg.entity_id.c_str(), msg.entity_id.size())) {
ESP_LOGW(TAG, "Ignoring an invalid entity id for wizard input");
return nullptr;
}
const char *value = msg.entity_id.c_str();
const size_t length = msg.entity_id.size();
for (size_t i = 0; i < API_WIZARD_INPUT_COUNT; i++) {
// The table is in flash, which ESP8266 can only read through progmem_memcpy
WizardInputEntry entry;
progmem_memcpy(&entry, &API_WIZARD_INPUTS[i], sizeof(entry));
if (entry.key != msg.key)
continue;
memcpy(entry.entity_id, msg.entity_id.c_str(), msg.entity_id.size());
entry.entity_id[msg.entity_id.size()] = '\0';
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
if (entry.size != 0) {
// Two passes, validate and measure then write, so a rejected request leaves the buffer unchanged
const size_t joined = wizard_join_entity_ids(value, length, nullptr);
if (joined == 0 || joined >= entry.size)
break;
wizard_join_entity_ids(value, length, entry.entity_id);
entry.entity_id[joined] = '\0';
return entry.entity_id;
}
#endif
if (!wizard_entity_id_valid(value, length))
break;
memcpy(entry.entity_id, value, length);
entry.entity_id[length] = '\0';
return entry.entity_id;
}
ESP_LOGW(TAG, "Ignoring an entity id for an unknown wizard input");
ESP_LOGW(TAG, "Ignoring an entity id for an unknown wizard input, or one that is not valid");
return nullptr;
}
#endif // USE_API_WIZARD_INPUTS
+33 -2
View File
@@ -6,6 +6,9 @@
#include <cstddef>
#include <cstdint>
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
#include <cstring>
#endif
#include "esphome/core/hal.h"
#include "proto.h"
@@ -26,7 +29,10 @@ extern const uint8_t API_WIZARD_DATA[] PROGMEM;
/// Where the entity id of an input is kept, found by the key the client uses for it.
struct WizardInputEntry {
uint32_t key; // FNV-1 hash of the ESPHome id of the input
char *entity_id; // RAM buffer of WIZARD_ENTITY_ID_BUFFER_SIZE bytes, shared with the homeassistant entity
char *entity_id; // RAM buffer, WIZARD_ENTITY_ID_BUFFER_SIZE bytes or size, shared with the homeassistant entity
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
uint16_t size; // Buffer size of a multiple input, 0 for a single input
#endif
};
/// The inputs of the wizard, in flash. API_WIZARD_INPUT_COUNT entries long.
@@ -44,9 +50,34 @@ const char *wizard_set_input(const WizardInputSetRequest &msg);
class WizardInput {
public:
explicit WizardInput(const char *entity_id) : entity_id_(entity_id) {}
/// The Home Assistant entity ID, empty until the wizard sets one.
/// The Home Assistant entity ID, empty until the wizard sets one. For a multiple input this is the whole
/// comma separated list, which homeassistant.action data can use as it is.
StringRef entity_id() const { return StringRef(this->entity_id_); }
bool has_entity_id() const { return this->entity_id_[0] != '\0'; }
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
/// The number of entity IDs in the list, 0 until the wizard sets one.
size_t entity_id_count() const {
if (!this->has_entity_id())
return 0;
size_t count = 1;
for (const char *p = this->entity_id_; (p = strchr(p, ',')) != nullptr; p++)
count++;
return count;
}
/// Call callback(StringRef) for each entity ID in the list, in order. The StringRef points into the list and is
/// not null terminated, so print it with "%.*s".
template<typename F> void for_each_entity_id(F &&callback) const {
// The device stores the list without spaces or empty entries
const char *start = this->entity_id_;
while (*start != '\0') {
const char *end = strchr(start, ',');
if (end == nullptr)
end = start + strlen(start);
callback(StringRef(start, end - start));
start = *end == ',' ? end + 1 : end;
}
}
#endif
protected:
const char *entity_id_;
+48 -7
View File
@@ -20,6 +20,8 @@ from esphome.const import (
CONF_ENTITY_ID,
CONF_ID,
CONF_INTERNAL,
CONF_MAX_LENGTH,
CONF_MULTIPLE,
CONF_NAME,
CONF_PAGES,
CONF_PLATFORM,
@@ -51,6 +53,10 @@ WizardInput = _API.class_("WizardInput")
WIZARD_ENTITY_ID_BUFFER_SIZE = (
256 # api_wizard.h; Home Assistant entity IDs are at most 255 bytes
)
WIZARD_ENTITY_ID_MAX_LENGTH = WIZARD_ENTITY_ID_BUFFER_SIZE - 1
# The value of a multiple input fits one WizardInputSetRequest, which the device accepts up to 8 KiB on ESP8266;
# api.proto max_data_length documents the same value
WIZARD_MULTIPLE_MAX_LENGTH = 4095
# One API message must fit APIBuffer::MAX_SIZE (65535) together with the largest frame header (7 bytes,
# Noise) and footer (16 bytes, Noise MAC)
WIZARD_RESPONSE_MAX_SIZE = 65535 - 7 - 16
@@ -106,6 +112,21 @@ WIZARD_ENTITY_SCHEMA = cv.Schema(
)
def _validate_multiple(conf: ConfigType) -> ConfigType:
if conf[CONF_MULTIPLE] and CONF_ENTITY in conf:
raise cv.Invalid(
"A linked input follows the state of exactly one Home Assistant entity, so multiple "
"is only for standalone inputs (id)",
path=[CONF_MULTIPLE],
)
if CONF_MAX_LENGTH in conf and not conf[CONF_MULTIPLE]:
raise cv.Invalid(
"max_length only applies to an input with multiple: true",
path=[CONF_MAX_LENGTH],
)
return conf
# An input is either standalone (id declares a new WizardInput) or linked to a homeassistant entity that has no
# entity_id of its own: Home Assistant sets it
WIZARD_INPUT_SCHEMA = cv.All(
@@ -114,6 +135,10 @@ WIZARD_INPUT_SCHEMA = cv.All(
cv.Optional(CONF_ID): cv.declare_id(WizardInput),
cv.Optional(CONF_ENTITY): cv.use_id(cg.EntityBase),
cv.Optional(CONF_DESCRIPTION): _wizard_text(WIZARD_DESCRIPTION_MAX_LENGTH),
cv.Optional(CONF_MULTIPLE, default=False): cv.boolean,
cv.Optional(CONF_MAX_LENGTH): cv.int_range(
min=WIZARD_ENTITY_ID_MAX_LENGTH, max=WIZARD_MULTIPLE_MAX_LENGTH
),
cv.Optional(CONF_TARGET): cv.Schema(
{
cv.Required(CONF_ENTITY): cv.All(
@@ -124,6 +149,7 @@ WIZARD_INPUT_SCHEMA = cv.All(
}
),
cv.has_exactly_one_key(CONF_ID, CONF_ENTITY),
_validate_multiple,
)
WIZARD_PAGE_SCHEMA = cv.All(
@@ -248,6 +274,8 @@ def _wizard_defines(wizard: ConfigType) -> set[str]:
defines.add("USE_API_WIZARD_LINKED_INPUTS")
if any(CONF_ID in conf for conf in inputs):
defines.add("USE_API_WIZARD_STANDALONE_INPUTS")
if any(conf.get(CONF_MULTIPLE) for conf in inputs):
defines.add("USE_API_WIZARD_MULTIPLE_INPUTS")
return defines
@@ -371,6 +399,10 @@ def _input_document(conf: ConfigType, config: fv.FinalValidateConfig) -> ConfigT
document: ConfigType = {"key": fnv1_hash(_wizard_input_id(conf).id)}
if description := conf.get(CONF_DESCRIPTION):
document[CONF_DESCRIPTION] = description
if conf.get(CONF_MULTIPLE):
document["multiple"] = True
if (max_length := conf.get(CONF_MAX_LENGTH)) is not None:
document["max_length"] = max_length
if filters := _wizard_input_filters(conf, config):
document["entity_filters"] = [dict(entity_filter) for entity_filter in filters]
return document
@@ -384,7 +416,7 @@ def wizard_document(wizard: ConfigType, config: fv.FinalValidateConfig) -> Confi
{"version": 1,
"pages": [{"title": "...", "description": "...",
"entities": [{"key": 123, "device_id": 456, "description": "..."}],
"inputs": [{"key": 789, "description": "...",
"inputs": [{"key": 789, "description": "...", "multiple": true, "max_length": 1023,
"entity_filters": [{"integration": "...", "domain": ["..."],
"device_class": ["..."], "supported_features": ["..."]}]}]}]}
@@ -396,6 +428,14 @@ def wizard_document(wizard: ConfigType, config: fv.FinalValidateConfig) -> Confi
the device it belongs to (esphome/core/config.py), and is left out for the main device.
- An input key is the FNV-1 hash of the ESPHome id of the input, or of the linked entity. A linked
entity must not set entity_id, as that is a static entry kept apart from the ones the wizard sets.
- multiple means the user may pick several entities. The client sends them in one
WizardInputSetRequest as a comma separated list ("calendar.a,calendar.b") with no spaces; the
device trims spaces around each id and stores the list without them. The whole request is
ignored when any id is empty or has no '.', or when the list without spaces does not fit
max_length (only on a multiple input, 255 when left out). A single input is ignored when the id
is empty, longer than 255 bytes or has no '.'. The version stays 1: a client that does not know
multiple ignores it, shows a single picker and sends one entity id, which a multiple input
accepts.
- entity_filters are the filters of the input, or the default filters of a linked switch,
number, text, select or button.
"""
@@ -441,14 +481,15 @@ async def to_code(wizard: ConfigType) -> None:
input_id = _wizard_input_id(conf)
# Every buffer starts empty, until the wizard sets it. A linked homeassistant entity uses it as its entity id.
buffer = _wizard_buffer_name(input_id)
cg.add_global(
cg.RawStatement(
f'static char {buffer}[{WIZARD_ENTITY_ID_BUFFER_SIZE}] = "";'
)
)
size = conf.get(CONF_MAX_LENGTH, WIZARD_ENTITY_ID_MAX_LENGTH) + 1
cg.add_global(cg.RawStatement(f'static char {buffer}[{size}] = "";'))
if CONF_ID in conf:
cg.new_Pvariable(input_id, cg.RawExpression(buffer))
entries.append(cg.RawExpression(f"{{{fnv1_hash(input_id.id)}u, {buffer}}}"))
key = fnv1_hash(input_id.id)
if conf[CONF_MULTIPLE]:
entries.append(cg.RawExpression(f"{{{key}u, {buffer}, {size}}}"))
else:
entries.append(cg.RawExpression(f"{{{key}u, {buffer}}}"))
if entries:
cg.extern_progmem_array(
"esphome::api::API_WIZARD_INPUTS",
+1 -1
View File
@@ -1,4 +1,5 @@
import esphome.codegen as cg
from esphome.components.const import CONF_AQI # noqa: F401
CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"]
DOMAIN = "aqi"
@@ -6,7 +7,6 @@ DOMAIN = "aqi"
aqi_ns = cg.esphome_ns.namespace("aqi")
AQICalculatorType = aqi_ns.enum("AQICalculatorType")
CONF_AQI = "aqi"
CONF_CALCULATION_TYPE = "calculation_type"
CONF_EXTENDED_RANGE = "extended_range"
@@ -3,6 +3,7 @@ from pathlib import Path
import esphome.codegen as cg
from esphome.components import audio, media_source, psram
from esphome.components.const import CONF_VERIFY_SSL
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
import esphome.final_validate as fv
@@ -16,7 +17,6 @@ AUTO_LOAD = ["audio"]
CONF_PERSISTENT_RING_BUFFER = "persistent_ring_buffer"
CONF_CA_CERTIFICATE_PATH = "ca_certificate_path"
CONF_HTTP_REQUEST = "http_request"
CONF_VERIFY_SSL = "verify_ssl"
audio_http_ns = cg.esphome_ns.namespace("audio_http")
AudioHTTPMediaSource = audio_http_ns.class_(
+1 -4
View File
@@ -146,10 +146,7 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
}
uint8_t checksum = 0;
// Calculate checksum and compare with signal value.
for (uint8_t i = 0; i < BALLU_STATE_LENGTH - 1; i++)
checksum += remote_state[i];
const uint8_t checksum = sum_bytes(remote_state, BALLU_STATE_LENGTH - 1);
if (checksum != remote_state[BALLU_STATE_LENGTH - 1]) {
ESP_LOGVV(TAG, "Checksum fail");
+2 -1
View File
@@ -607,7 +607,8 @@ async def setup_binary_sensor_core_(var, config):
if filters_config := config.get(CONF_FILTERS):
cg.add_define("USE_BINARY_SENSOR_FILTER")
filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config)
cg.add(var.add_filters(filters))
for filter_ in filters:
cg.add(var.add_filter(filter_))
CORE.add_job(_build_binary_sensor_automations, var, config)
+1 -5
View File
@@ -51,12 +51,8 @@ void BL0939::loop() {
}
bool BL0939::validate_checksum(const DataPacket *data) {
uint8_t checksum = BL0939_READ_COMMAND;
// Whole package but checksum
for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) {
checksum += data->raw[i];
}
checksum ^= 0xFF;
const uint8_t checksum = sum_bytes(data->raw, sizeof(data->raw) - 1, BL0939_READ_COMMAND) ^ 0xFF;
if (checksum != data->checksum) {
ESP_LOGW(TAG, "BL0939 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
}
+2 -6
View File
@@ -44,13 +44,9 @@ void BL0940::loop() {
}
bool BL0940::validate_checksum_(DataPacket *data) {
uint8_t checksum = this->read_command_;
// Whole package but checksum
uint8_t *raw = (uint8_t *) data;
for (uint32_t i = 0; i < sizeof(*data) - 1; i++) {
checksum += raw[i];
}
checksum ^= 0xFF;
const uint8_t checksum =
sum_bytes(reinterpret_cast<const uint8_t *>(data), sizeof(*data) - 1, this->read_command_) ^ 0xFF;
if (checksum != data->checksum) {
ESP_LOGW(TAG, "Invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
}
+3 -6
View File
@@ -70,13 +70,10 @@ void BL0942::loop() {
}
bool BL0942::validate_checksum_(DataPacket *data) {
uint8_t checksum = BL0942_READ_COMMAND | this->address_;
// Whole package but checksum
uint8_t *raw = (uint8_t *) data;
for (uint32_t i = 0; i < sizeof(*data) - 1; i++) {
checksum += raw[i];
}
checksum ^= 0xFF;
const uint8_t checksum =
sum_bytes(reinterpret_cast<const uint8_t *>(data), sizeof(*data) - 1, BL0942_READ_COMMAND | this->address_) ^
0xFF;
if (checksum != data->checksum) {
ESP_LOGW(TAG, "BL0942 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
}
+2 -1
View File
@@ -3,6 +3,7 @@ from esphome.components.logger import request_log_listener
from esphome.components.uart import (
UARTComponent,
debug_to_code,
mark_unclocked,
maybe_empty_debug,
uart_ns,
)
@@ -42,7 +43,7 @@ def validate_rx_buffer(config: ConfigType) -> ConfigType:
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLENUS),
cv.GenerateID(): cv.All(cv.declare_id(BLENUS), mark_unclocked),
cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of(
*[CONF_LOGS, CONF_UART], lower=True
),
+31 -6
View File
@@ -1,14 +1,17 @@
import logging
import re
from typing import Any
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID
from esphome.const import CONF_DATA, CONF_ID, CONF_PLATFORM, CONF_TRIGGER_ID
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@mvturnho", "@danielschramm"]
DOMAIN = "canbus"
IS_PLATFORM_COMPONENT = True
@@ -20,6 +23,9 @@ CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request"
CONF_CANBUS_ID = "canbus_id"
CONF_BIT_RATE = "bit_rate"
CONF_ON_FRAME = "on_frame"
CONF_MAX_FRAMES_PER_LOOP = "max_frames_per_loop"
DEFAULT_MAX_FRAMES_PER_LOOP = 50 # Keep in sync with max_frames_per_loop_ in canbus.h
DEFAULT_BIT_RATE = "125KBPS" # Keep in sync with bit_rate_ in canbus.h
def validate_id(config: ConfigType) -> ConfigType:
@@ -87,8 +93,13 @@ CANBUS_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CanbusComponent),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_BIT_RATE, default="125KBPS"): cv.enum(CAN_SPEEDS, upper=True),
cv.Optional(CONF_BIT_RATE, default=DEFAULT_BIT_RATE): cv.enum(
CAN_SPEEDS, upper=True
),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(
CONF_MAX_FRAMES_PER_LOOP, default=DEFAULT_MAX_FRAMES_PER_LOOP
): cv.positive_not_null_int,
cv.Optional(CONF_ON_FRAME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger),
@@ -109,9 +120,13 @@ CANBUS_SCHEMA.add_extra(validate_id)
async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
cg.add(var.set_can_id([config[CONF_CAN_ID]]))
cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]]))
cg.add(var.set_bitrate(CAN_SPEEDS[config[CONF_BIT_RATE]]))
cg.add(var.set_can_id(config[CONF_CAN_ID]))
if config[CONF_USE_EXTENDED_ID]:
cg.add(var.set_use_extended_id(True))
if (bit_rate := config[CONF_BIT_RATE]) != DEFAULT_BIT_RATE:
cg.add(var.set_bitrate(CAN_SPEEDS[bit_rate]))
if (max_frames := config[CONF_MAX_FRAMES_PER_LOOP]) != DEFAULT_MAX_FRAMES_PER_LOOP:
cg.add(var.set_max_frames_per_loop(max_frames))
for conf in config.get(CONF_ON_FRAME, []):
can_id = conf[CONF_CAN_ID]
@@ -138,9 +153,19 @@ async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
)
async def register_canbus(var: MockObj, config: ConfigType) -> None:
async def register_canbus(
var: MockObj, config: ConfigType, *, reports_status: bool | None = None
) -> None:
if not CORE.has_id(config[CONF_ID]):
var = cg.new_Pvariable(config[CONF_ID], var)
if reports_status is None:
_LOGGER.warning(
"CAN bus platform '%s' does not report bus state, so bus-off recovery and "
"error-state logging are disabled. Platform authors should either override "
"get_status() and get_events() and call register_canbus(..., reports_status=True) "
"or pass reports_status=False if the hardware cannot report it",
config[CONF_PLATFORM],
)
await setup_canbus_core_(var, config)
+73 -7
View File
@@ -1,5 +1,6 @@
#include "canbus.h"
#include <algorithm>
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome::canbus {
@@ -65,17 +66,25 @@ void Canbus::add_trigger(CanbusTrigger *trigger) {
};
void Canbus::loop() {
uint32_t now = App.get_loop_component_start_time();
if ((now - this->last_event_check_time_) >= EVENT_CHECK_INTERVAL_MS) {
this->log_events_(this->get_events(), now);
this->last_event_check_time_ = now;
}
struct CanFrame can_message;
// read all messages until queue is empty
int message_counter = 0;
while (this->read_message(&can_message) == canbus::ERROR_OK) {
message_counter++;
uint32_t message_counter = 0;
for (; message_counter < this->max_frames_per_loop_; message_counter++) {
if (this->read_message(&can_message) != canbus::ERROR_OK) {
break;
}
if (can_message.use_extended_id) {
ESP_LOGD(TAG, "received can message (#%d) extended can_id=0x%" PRIx32 " size=%d", message_counter,
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") extended can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
} else {
ESP_LOGD(TAG, "received can message (#%d) std can_id=0x%" PRIx32 " size=%d", message_counter, can_message.can_id,
can_message.can_data_length_code);
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") std can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
}
std::vector<uint8_t> data;
@@ -99,6 +108,63 @@ void Canbus::loop() {
}
}
}
if (message_counter == this->max_frames_per_loop_) {
ESP_LOGW(TAG,
"Reached max_frames_per_loop=%" PRIu32 ", deferring remaining CAN frames. Some frames might be dropped",
this->max_frames_per_loop_);
}
}
void Canbus::log_events_(CanEventFlags events, uint32_t now) {
// Bus-off gets its own handling because automatic recovery can make it flap
if (events & CanEventFlags::CAN_EVENT_BUS_OFF) {
this->last_bus_off_time_ = now;
if (!this->bus_off_) {
this->bus_off_ = true;
this->status_set_warning(LOG_STR("Bus off"));
}
} else if (this->bus_off_) {
if ((now - this->last_bus_off_time_) >= EVENT_LOG_BUS_OFF_HOLDOFF_MS) {
// No bus-off event for the hold-off time, so ask whether it is really over
if (this->get_status().bus_off) {
this->last_bus_off_time_ = now;
} else {
this->bus_off_ = false;
this->status_clear_warning();
// Events gathered while bus-off say nothing about the recovered bus
this->events_to_log_ = 0;
}
}
} else {
this->events_to_log_ |= events;
if (this->events_to_log_ != 0 && (now - this->last_event_log_time_) >= EVENT_LOG_THROTTLE_MS) {
if (this->events_to_log_ & (CanEventFlags::CAN_EVENT_PASSIVE | CanEventFlags::CAN_EVENT_ACTIVE)) {
bool error_passive = this->events_to_log_ & CanEventFlags::CAN_EVENT_PASSIVE;
if (error_passive && (this->events_to_log_ & CanEventFlags::CAN_EVENT_ACTIVE)) {
// Both edges within one throttle window, so the counters decide the current state
auto status = this->get_status();
error_passive = status.rx_error_counter >= CAN_ERROR_PASSIVE_THRESHOLD ||
status.tx_error_counter >= CAN_ERROR_PASSIVE_THRESHOLD;
}
ESP_LOGW(TAG, "entered %s", error_passive ? LOG_STR_LITERAL("error-passive") : LOG_STR_LITERAL("error-active"));
}
if (this->events_to_log_ & CanEventFlags::CAN_EVENT_RX_QUEUE_FULL) {
ESP_LOGD(TAG, "receive buffer overrun");
}
this->events_to_log_ = 0;
this->last_event_log_time_ = now;
}
}
#ifdef ESPHOME_LOG_HAS_VERBOSE
if ((now - this->last_state_log_time_) >= STATE_LOG_INTERVAL_MS) {
auto status = this->get_status();
ESP_LOGV(TAG, "Status: bus_off %d, tx_err %d, rx_err %d", status.bus_off, status.tx_error_counter,
status.rx_error_counter);
this->last_state_log_time_ = now;
}
#endif
}
} // namespace esphome::canbus
+48 -2
View File
@@ -48,6 +48,8 @@ template<typename... Ts> class CanbusSendAction;
/* CAN payload length definitions according to ISO 11898-1 */
static const uint8_t CAN_MAX_DATA_LENGTH = 8;
static const uint8_t CAN_ERROR_PASSIVE_THRESHOLD = 128;
/*
Can Frame describes a normative CAN Frame
The RTR = Remote Transmission Request is implemented in every CAN controller but rarely used
@@ -61,6 +63,26 @@ struct CanFrame {
uint8_t data[CAN_MAX_DATA_LENGTH] __attribute__((aligned(8)));
};
enum CanEventFlags : uint8_t {
CAN_EVENT_PASSIVE = 1 << 0,
CAN_EVENT_ACTIVE = 1 << 1,
CAN_EVENT_BUS_OFF = 1 << 2,
CAN_EVENT_RX_QUEUE_FULL = 1 << 3,
};
static constexpr uint32_t CAN_STATUS_UNSET = UINT32_MAX;
struct CanStatus {
bool bus_off{false};
uint8_t rx_error_counter{0};
uint8_t tx_error_counter{0};
uint32_t tx_failed_count{CAN_STATUS_UNSET};
uint32_t rx_missed_count{CAN_STATUS_UNSET};
uint32_t rx_overrun_count{CAN_STATUS_UNSET};
uint32_t arb_lost_count{CAN_STATUS_UNSET};
uint32_t bus_error_count{CAN_STATUS_UNSET};
};
class Canbus : public Component {
public:
Canbus(){};
@@ -82,6 +104,8 @@ class Canbus : public Component {
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
void set_max_frames_per_loop(uint32_t max_frames_per_loop) { this->max_frames_per_loop_ = max_frames_per_loop; }
virtual CanStatus get_status() { return {}; }
void add_trigger(CanbusTrigger *trigger);
/**
@@ -100,14 +124,36 @@ class Canbus : public Component {
template<typename... Ts> friend class CanbusSendAction;
std::vector<CanbusTrigger *> triggers_{};
uint32_t can_id_;
bool use_extended_id_;
CanSpeed bit_rate_;
uint32_t max_frames_per_loop_{50}; // Keep in sync with DEFAULT_MAX_FRAMES_PER_LOOP in __init__.py
CanSpeed bit_rate_{CAN_125KBPS}; // Keep in sync with DEFAULT_BIT_RATE in __init__.py
bool use_extended_id_{false};
uint8_t events_to_log_{0};
bool bus_off_{false};
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{};
// interval in which to check for new can bus events (bus-off, passive, active)
static constexpr uint32_t EVENT_CHECK_INTERVAL_MS = 100;
uint32_t last_event_check_time_{0};
// time to wait after logging an event before logging the next event(s)
static constexpr uint32_t EVENT_LOG_THROTTLE_MS = 1000;
uint32_t last_event_log_time_{0};
// time span without bus-off events before the bus is declared recovered
static constexpr uint32_t EVENT_LOG_BUS_OFF_HOLDOFF_MS = 1000;
uint32_t last_bus_off_time_{0};
#ifdef ESPHOME_LOG_HAS_VERBOSE
static constexpr uint32_t STATE_LOG_INTERVAL_MS = 1000;
uint32_t last_state_log_time_{0};
#endif
void log_events_(CanEventFlags events, uint32_t now);
virtual bool setup_internal() = 0;
virtual Error send_message(struct CanFrame *frame) = 0;
virtual Error read_message(struct CanFrame *frame) = 0;
virtual CanEventFlags get_events() { return CanEventFlags{}; }
};
template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, public Parented<Canbus> {
+60 -21
View File
@@ -6,10 +6,16 @@
namespace esphome::cm1106 {
ESPHOME_LOG_TAG(TAG, "cm1106");
static const uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
static constexpr uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
static constexpr uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
static constexpr uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
// The factory default ABC calibration cycle differs between CM1106 models, so
// every ABC write applies the configured cycle and baseline explicitly.
static constexpr uint8_t C_M1106_CMD_SET_ABC_STATUS[10] = {0x11, 0x07, 0x10, 0x64, 0x00, 0x0F, 0x01, 0x90, 0x64, 0x00};
static constexpr uint8_t C_M1106_CMD_SET_ABC_STATUS_RESPONSE[4] = {0x16, 0x01, 0x10, 0xD9};
static constexpr uint8_t CM1106_ABC_FLAG_ENABLE = 0x0;
static constexpr uint8_t CM1106_ABC_FLAG_DISABLE = 0x2;
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len - 1; i++) {
@@ -25,34 +31,46 @@ void CM1106Component::setup() {
this->mark_failed();
return;
}
if (this->abc_boot_logic_ != CM1106_ABC_NONE) {
this->abc_set_(this->abc_boot_logic_ == CM1106_ABC_ENABLED);
}
}
void CM1106Component::update() {
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
ESP_LOGW(TAG, "Reading data failed!");
this->status_set_warning();
return;
}
if (response[0] != 0x16 || response[1] != 0x05 || response[2] != 0x01) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
response[3]);
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
this->status_set_warning();
return;
}
uint8_t checksum = cm1106_checksum(response, sizeof(response));
if (response[7] != checksum) {
ESP_LOGW(TAG, "CM1106 Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
ESP_LOGW(TAG, "Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
this->status_set_warning();
return;
}
const uint16_t ppm = response[3] << 8 | response[4];
const uint8_t status = response[5];
if (status & 0x1) {
ESP_LOGW(TAG, "Preheating; CO₂=%uppm", ppm);
this->status_set_warning();
return;
}
if (status) {
ESP_LOGW(TAG, "Status: %02X", status);
}
this->status_clear_warning();
uint16_t ppm = response[3] << 8 | response[4];
ESP_LOGD(TAG, "CM1106 Received CO₂=%uppm DF3=%02X DF4=%02X", ppm, response[5], response[6]);
ESP_LOGD(TAG, "CO₂=%uppm DF4=%02X", ppm, response[6]);
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(ppm);
}
@@ -62,24 +80,38 @@ void CM1106Component::calibrate_zero(uint16_t ppm) {
memcpy(cmd, C_M1106_CMD_SET_CO2_CALIB, sizeof(cmd));
cmd[3] = ppm >> 8;
cmd[4] = ppm & 0xFF;
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_CO2_CALIB_RESPONSE)) {
ESP_LOGD(TAG, "Successfully calibrated sensor to %uppm", ppm);
}
}
void CM1106Component::abc_set_(bool enabled) {
ESP_LOGD(TAG, "%sabling automatic baseline calibration", enabled ? LOG_STR_LITERAL("En") : LOG_STR_LITERAL("Dis"));
uint8_t cmd[10];
memcpy(cmd, C_M1106_CMD_SET_ABC_STATUS, sizeof(cmd));
cmd[4] = enabled ? CM1106_ABC_FLAG_ENABLE : CM1106_ABC_FLAG_DISABLE;
cmd[5] = this->abc_cycle_;
cmd[6] = this->abc_baseline_ >> 8;
cmd[7] = this->abc_baseline_ & 0xFF;
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_ABC_STATUS_RESPONSE)) {
ESP_LOGD(TAG, "Successfully set ABC status");
}
}
bool CM1106Component::send_command_expect_ack_(const uint8_t *command, size_t command_len, const uint8_t *ack) {
uint8_t response[4] = {0};
if (!this->cm1106_write_command_(cmd, sizeof(cmd), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
if (!this->cm1106_write_command_(command, command_len, response, sizeof(response))) {
ESP_LOGW(TAG, "Command %02X failed", command[2]);
this->status_set_warning();
return;
return false;
}
// check if correct response received
if (memcmp(response, C_M1106_CMD_SET_CO2_CALIB_RESPONSE, sizeof(response)) != 0) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
response[3]);
if (memcmp(response, ack, sizeof(response)) != 0) {
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
this->status_set_warning();
return;
return false;
}
this->status_clear_warning();
ESP_LOGD(TAG, "CM1106 Successfully calibrated sensor to %uppm", ppm);
return true;
}
bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response,
@@ -100,6 +132,13 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
void CM1106Component::dump_config() {
ESP_LOGCONFIG(TAG, "CM1106:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
if (this->abc_boot_logic_ != CM1106_ABC_NONE) {
ESP_LOGCONFIG(TAG,
" Automatic baseline calibration on boot: %s\n"
" ABC calibration cycle: %u days\n"
" ABC baseline: %uppm",
ONOFF(this->abc_boot_logic_ == CM1106_ABC_ENABLED), this->abc_cycle_, this->abc_baseline_);
}
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
+23 -2
View File
@@ -6,6 +6,14 @@
namespace esphome::cm1106 {
// ABC logic requested at boot; CM1106_ABC_NONE means nothing is sent and the
// sensor keeps its stored setting.
enum CM1106ABCLogic : uint8_t {
CM1106_ABC_NONE = 0,
CM1106_ABC_ENABLED,
CM1106_ABC_DISABLED,
};
class CM1106Component final : public PollingComponent, public uart::UARTDevice {
public:
void setup() override;
@@ -13,13 +21,26 @@ class CM1106Component final : public PollingComponent, public uart::UARTDevice {
void dump_config() override;
void calibrate_zero(uint16_t ppm);
void abc_enable() { this->abc_set_(true); }
void abc_disable() { this->abc_set_(false); }
void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; }
void set_abc(bool enabled, uint8_t cycle_days, uint16_t baseline_ppm) {
this->abc_boot_logic_ = enabled ? CM1106_ABC_ENABLED : CM1106_ABC_DISABLED;
this->abc_cycle_ = cycle_days;
this->abc_baseline_ = baseline_ppm;
}
protected:
sensor::Sensor *co2_sensor_{nullptr};
bool cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, size_t response_len);
/// Sends a command and checks the sensor's four byte acknowledgement, updating the warning status.
bool send_command_expect_ack_(const uint8_t *command, size_t command_len, const uint8_t *ack);
void abc_set_(bool enabled);
sensor::Sensor *co2_sensor_{nullptr};
CM1106ABCLogic abc_boot_logic_{CM1106_ABC_NONE};
uint8_t abc_cycle_{15};
uint16_t abc_baseline_{400};
};
} // namespace esphome::cm1106
+29
View File
@@ -4,9 +4,12 @@ from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.components.const import CONF_AUTOMATIC_BASELINE_CALIBRATION
import esphome.config_validation as cv
from esphome.const import (
CONF_BASELINE,
CONF_CO2,
CONF_CYCLE,
CONF_ID,
DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2,
@@ -18,11 +21,22 @@ from esphome.types import ConfigType
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@andrewjswan"]
# true enables ABC with these defaults, false disables it, a mapping enables it with custom values
_ABC_SETTINGS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_CYCLE, default=15): cv.int_range(min=1, max=90),
cv.Optional(CONF_BASELINE, default=400): cv.int_range(min=400, max=10000),
}
)
cm1106_ns = cg.esphome_ns.namespace("cm1106")
CM1106Component = cm1106_ns.class_(
"CM1106Component", cg.PollingComponent, uart.UARTDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
@@ -34,6 +48,9 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.Any(
cv.boolean, _ABC_SETTINGS_SCHEMA
),
},
)
.extend(cv.polling_component_schema("60s"))
@@ -57,6 +74,11 @@ async def to_code(config: ConfigType) -> None:
sensors = sensor.sub_sensors(config)
await sensors(CONF_CO2, var.set_co2_sensor)
if (abc := config.get(CONF_AUTOMATIC_BASELINE_CALIBRATION)) is not None:
enabled = abc if isinstance(abc, bool) else True
settings = _ABC_SETTINGS_SCHEMA({}) if isinstance(abc, bool) else abc
cg.add(var.set_abc(enabled, settings[CONF_CYCLE], settings[CONF_BASELINE]))
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
{
@@ -70,3 +92,10 @@ automation.register_apply_action(
CALIBRATION_ACTION_SCHEMA,
automation.ApplyCall("calibrate_zero(400)"),
)
for _name, _call in (
("cm1106.abc_enable", "abc_enable()"),
("cm1106.abc_disable", "abc_disable()"),
):
automation.register_apply_action(
_name, CALIBRATION_ACTION_SCHEMA, automation.ApplyCall(_call)
)
+10
View File
@@ -8,11 +8,17 @@ BYTE_ORDER_BIG = "big_endian"
CONF_ACCELEROMETER_ODR = "accelerometer_odr"
CONF_ACCELEROMETER_RANGE = "accelerometer_range"
CONF_ADC_AVERAGING = "adc_averaging"
CONF_ADC_TIME = "adc_time"
CONF_ALLOWED_IPS = "allowed_ips"
CONF_AQI = "aqi"
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
CONF_B_CONSTANT = "b_constant"
CONF_BLUE_CHANNEL = "blue_channel"
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
CONF_BYTE_ORDER = "byte_order"
CONF_CHANNEL_COLORS = "channel_colors"
CONF_CLEAR_CHANNEL = "clear_channel"
CONF_CLIMATE_ID = "climate_id"
CONF_CO2_EQUIVALENT = "co2_equivalent"
CONF_COLOR_DEPTH = "color_depth"
@@ -25,6 +31,7 @@ CONF_DESCRIPTION = "description"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
CONF_GREEN_CHANNEL = "green_channel"
CONF_GYROSCOPE_ODR = "gyroscope_odr"
CONF_GYROSCOPE_RANGE = "gyroscope_range"
CONF_HOLD_STATE = "hold_state"
@@ -46,6 +53,7 @@ CONF_ON_STATE_CHANGE = "on_state_change"
CONF_PARITY = "parity"
CONF_RECEIVER_FREQUENCY = "receiver_frequency"
CONF_RECONNECT_INTERVAL = "reconnect_interval"
CONF_RED_CHANNEL = "red_channel"
CONF_REQUEST_HEADERS = "request_headers"
CONF_ROLE = "role"
CONF_ROWS = "rows"
@@ -56,6 +64,7 @@ CONF_STATE_SAVE_INTERVAL = "state_save_interval"
CONF_STOP_BITS = "stop_bits"
CONF_TARGET_COUNT = "target_count"
CONF_USE_PSRAM = "use_psram"
CONF_VERIFY_SSL = "verify_ssl"
CONF_VOC_INDEX = "voc_index"
CONF_VOLUME_INCREMENT = "volume_increment"
CONF_VOLUME_INITIAL = "volume_initial"
@@ -71,3 +80,4 @@ KEY_METADATA = "metadata"
UNIT_AMPERE_HOUR = "Ah"
UNIT_COUNTS = "#"
UNIT_MILLIAMPERE_HOUR = "mAh"
+78 -28
View File
@@ -18,21 +18,32 @@ static constexpr int CSE7761_UREF = 42563; // RmsUc
static constexpr int CSE7761_IREF = 52241; // RmsIAC
static constexpr int CSE7761_PREF = 44513; // PowerPAC
// System clock (3.579545MHz) / 8, divided by Ufreq gives the line frequency in Hz
static constexpr float CSE7761_UFREQ_CLOCK = 3579545.0f / 8.0f;
static constexpr uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04)
static constexpr uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000)
static constexpr uint8_t CSE7761_REG_HFCONST = 0x02; // (2) Pulse frequency register (0x1000)
static constexpr uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001)
static constexpr uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210)
static constexpr uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000)
static constexpr uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000)
static constexpr uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000)
static constexpr uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000)
static constexpr uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000)
static constexpr uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register
static constexpr uint8_t CSE7761_REG_UFREQ = 0x23; // (2) Voltage frequency (0x0000)
static constexpr uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000)
static constexpr uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000)
static constexpr uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000)
static constexpr uint8_t CSE7761_REG_POWERFACTOR = 0x27; // (3) Power factor of the selected channel (0x7FFFFF)
static constexpr uint8_t CSE7761_REG_ENERGY_PA = 0x28; // (3) Channel A active energy pulse count (0x000000)
static constexpr uint8_t CSE7761_REG_ENERGY_PB = 0x29; // (3) Channel B active energy pulse count (0x000000)
static constexpr uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000)
static constexpr uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000)
static constexpr uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register
static constexpr uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum
static constexpr uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient
// Datasheet: Wh = pulses * EnergyXC * HFConst * 1000 / 2^41 with K1 = K2 = 1
static constexpr float CSE7761_ENERGY_WH_SCALE = 1000.0f / 2199023255552.0f;
static constexpr uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command
static constexpr uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets
static constexpr uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation
@@ -58,6 +69,15 @@ void CSE7761Component::dump_config() {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
LOG_SENSOR(" ", "Current 1", this->current_sensor_1_);
LOG_SENSOR(" ", "Current 2", this->current_sensor_2_);
LOG_SENSOR(" ", "Power 1", this->power_sensor_1_);
LOG_SENSOR(" ", "Power 2", this->power_sensor_2_);
LOG_SENSOR(" ", "Frequency", this->frequency_sensor_);
LOG_SENSOR(" ", "Power Factor 1", this->power_factor_sensor_1_);
LOG_SENSOR(" ", "Energy 1", this->energy_sensors_[0]);
LOG_SENSOR(" ", "Energy 2", this->energy_sensors_[1]);
}
void CSE7761Component::update() {
@@ -81,11 +101,7 @@ void CSE7761Component::write_(uint8_t reg, uint16_t data) {
buffer[3] = data & 0xFF;
len = 4;
}
uint8_t crc = 0;
for (uint32_t i = 0; i < len; i++) {
crc += buffer[i];
}
buffer[len] = ~crc;
buffer[len] = ~sum_bytes(buffer, len);
len++;
}
@@ -115,33 +131,30 @@ bool CSE7761Component::read_once_(uint8_t reg, uint8_t size, uint32_t *value) {
}
rcvd--;
if (static_cast<uint8_t>(~sum_bytes(buffer, rcvd, 0xA5 + reg)) != buffer[rcvd]) {
return false;
}
uint32_t result = 0;
// CRC check
uint8_t crc = 0xA5 + reg;
for (uint32_t i = 0; i < rcvd; i++) {
result = (result << 8) | buffer[i];
crc += buffer[i];
}
crc = ~crc;
if (crc != buffer[rcvd]) {
return false;
}
*value = result;
return true;
}
uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) {
bool result = false; // Start loop
uint8_t retry = 3; // Retry up to three times
uint32_t value = 0; // Default no value
while (!result && retry > 0) {
retry--;
if (this->read_once_(reg, size, &value))
return value;
bool CSE7761Component::read_(uint8_t reg, uint8_t size, uint32_t *value) {
for (uint8_t retry = 0; retry < 3; retry++) {
if (this->read_once_(reg, size, value))
return true;
}
ESP_LOGE(TAG, "Reading register %hhu failed!", reg);
return value;
return false;
}
uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) {
uint32_t value = 0;
return this->read_(reg, size, &value) ? value : 0;
}
uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) {
@@ -167,12 +180,25 @@ bool CSE7761Component::chip_init_() {
calc_chksum += this->data_.coefficient[i];
}
calc_chksum = ~calc_chksum;
const bool energy_configured = this->energy_sensors_[0] != nullptr || this->energy_sensors_[1] != nullptr;
uint16_t coeff_chksum = this->read_(CSE7761_REG_COEFFCHKSUM, 2);
if ((calc_chksum != coeff_chksum) || (!calc_chksum)) {
ESP_LOGD(TAG, "Default calibration");
this->data_.coefficient[RMS_IAC] = CSE7761_IREF;
this->data_.coefficient[RMS_UC] = CSE7761_UREF;
this->data_.coefficient[POWER_PAC] = CSE7761_PREF;
if (energy_configured) {
ESP_LOGW(TAG, "No energy calibration, energy unavailable");
}
} else if (energy_configured) {
uint32_t hf_const = 0;
if (!this->read_(CSE7761_REG_HFCONST, 2, &hf_const)) {
ESP_LOGW(TAG, "HFConst read failed, energy unavailable");
}
for (uint8_t channel = 0; channel < 2; channel++) {
uint32_t scale = this->data_.coefficient[ENERGY_AC + channel] * hf_const;
this->energy_[channel].wh_per_pulse = static_cast<float>(scale) * CSE7761_ENERGY_WH_SCALE;
}
}
this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_ENABLE_WRITE);
@@ -181,7 +207,8 @@ bool CSE7761Component::chip_init_() {
if (sys_status & 0x10) { // Write enable to protected registers (WREN)
this->write_(CSE7761_REG_SYSCON | 0x80, 0xFF04);
this->write_(CSE7761_REG_EMUCON | 0x80, 0x1183);
this->write_(CSE7761_REG_EMUCON2 | 0x80, 0x0FC1);
// WaveEN|ZxEN, needed for Ufreq
this->write_(CSE7761_REG_EMUCON2 | 0x80, this->frequency_sensor_ != nullptr ? 0x0FE5 : 0x0FC1);
this->write_(CSE7761_REG_PULSE1SEL | 0x80, 0x3290);
} else {
ESP_LOGD(TAG, "Write failed at chip_init");
@@ -218,12 +245,35 @@ void CSE7761Component::get_data_() {
this->voltage_sensor_->publish_state(voltage);
}
uint32_t raw_reg = 0;
if (this->frequency_sensor_ != nullptr && this->read_(CSE7761_REG_UFREQ, 2, &raw_reg)) {
// Ufreq highest bit set means no signal
float frequency = (raw_reg == 0 || raw_reg >= 0x8000) ? 0.0f : CSE7761_UFREQ_CLOCK / raw_reg;
this->frequency_sensor_->publish_state(frequency);
}
if (this->power_factor_sensor_1_ != nullptr && this->read_(CSE7761_REG_POWERFACTOR, 3, &raw_reg)) {
// ADC2ON makes the chip report channel A
int24_t pf{static_cast<int32_t>(raw_reg)};
this->power_factor_sensor_1_->publish_state(static_cast<float>(static_cast<int32_t>(pf)) / 0x800000);
}
for (uint8_t channel = 0; channel < 2; channel++) {
// Active power = PowerPA * PowerPAC * 1000 / 0x80000000
float active_power =
static_cast<float>(this->data_.active_power[channel]) / this->coefficient_by_unit_(POWER_PAC); // W
float amps = static_cast<float>(this->data_.current_rms[channel]) / this->coefficient_by_unit_(RMS_IAC); // A
ESP_LOGD(TAG, "Channel %d power %f W, current %f A", channel + 1, active_power, amps);
sensor::Sensor *energy_sensor = this->energy_sensors_[channel];
CSE7761EnergyChannel &energy = this->energy_[channel];
uint8_t energy_reg = (channel == 0) ? CSE7761_REG_ENERGY_PA : CSE7761_REG_ENERGY_PB;
uint32_t raw = 0;
if (energy_sensor != nullptr && energy.wh_per_pulse > 0.0f && this->read_(energy_reg, 3, &raw)) {
// 24-bit counter, not cleared on read
energy.pulses += (raw - energy.last_pulses) & 0xFFFFFF;
energy.last_pulses = raw;
energy_sensor->publish_state(energy.pulses * energy.wh_per_pulse);
}
if (channel == 0) {
if (this->power_sensor_1_ != nullptr) {
this->power_sensor_1_->publish_state(active_power);
+18 -1
View File
@@ -3,11 +3,11 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/datatypes.h"
namespace esphome::cse7761 {
struct CSE7761DataStruct {
uint32_t frequency = 0;
uint32_t voltage_rms = 0;
uint32_t current_rms[2] = {0};
int32_t active_power[2] = {0};
@@ -15,14 +15,26 @@ struct CSE7761DataStruct {
bool ready = false;
};
struct CSE7761EnergyChannel {
uint32_t last_pulses{0};
uint32_t pulses{0};
float wh_per_pulse{0.0f};
};
/// This class implements support for the CSE7761 UART power sensor.
class CSE7761Component final : public PollingComponent, public uart::UARTDevice {
public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
void set_power_factor_1_sensor(sensor::Sensor *power_factor_sensor_1) {
power_factor_sensor_1_ = power_factor_sensor_1;
}
void set_active_power_1_sensor(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; }
void set_current_1_sensor(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; }
void set_energy_1_sensor(sensor::Sensor *energy_sensor_1) { this->energy_sensors_[0] = energy_sensor_1; }
void set_active_power_2_sensor(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; }
void set_current_2_sensor(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; }
void set_energy_2_sensor(sensor::Sensor *energy_sensor_2) { this->energy_sensors_[1] = energy_sensor_2; }
void setup() override;
void dump_config() override;
void update() override;
@@ -30,14 +42,19 @@ class CSE7761Component final : public PollingComponent, public uart::UARTDevice
protected:
// Sensors
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *frequency_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_1_{nullptr};
sensor::Sensor *power_sensor_1_{nullptr};
sensor::Sensor *current_sensor_1_{nullptr};
sensor::Sensor *power_sensor_2_{nullptr};
sensor::Sensor *current_sensor_2_{nullptr};
sensor::Sensor *energy_sensors_[2]{nullptr, nullptr};
CSE7761DataStruct data_;
CSE7761EnergyChannel energy_[2];
void write_(uint8_t reg, uint16_t data);
bool read_once_(uint8_t reg, uint8_t size, uint32_t *value);
bool read_(uint8_t reg, uint8_t size, uint32_t *value);
uint32_t read_(uint8_t reg, uint8_t size);
uint32_t coefficient_by_unit_(uint32_t unit);
bool chip_init_();
+43 -12
View File
@@ -2,15 +2,22 @@ import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_FREQUENCY,
CONF_ID,
CONF_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_HERTZ,
UNIT_VOLT,
UNIT_WATT,
UNIT_WATT_HOURS,
)
from esphome.types import ConfigType
@@ -26,6 +33,9 @@ CONF_CURRENT_1 = "current_1"
CONF_CURRENT_2 = "current_2"
CONF_ACTIVE_POWER_1 = "active_power_1"
CONF_ACTIVE_POWER_2 = "active_power_2"
CONF_ENERGY_1 = "energy_1"
CONF_ENERGY_2 = "energy_2"
CONF_POWER_FACTOR_1 = "power_factor_1"
CONFIG_SCHEMA = (
cv.Schema(
@@ -37,6 +47,17 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,
accuracy_decimals=2,
device_class=DEVICE_CLASS_FREQUENCY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_FACTOR_1): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CURRENT_1): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
@@ -61,6 +82,18 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ENERGY_1): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_ENERGY_2): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
}
)
.extend(cv.polling_component_schema("60s"))
@@ -83,15 +116,13 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for key in [
CONF_VOLTAGE,
CONF_CURRENT_1,
CONF_CURRENT_2,
CONF_ACTIVE_POWER_1,
CONF_ACTIVE_POWER_2,
]:
if key not in config:
continue
conf = config[key]
sens = await sensor.new_sensor(conf)
cg.add(getattr(var, f"set_{key}_sensor")(sens))
sensors = sensor.sub_sensors(config)
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
await sensors(CONF_FREQUENCY, var.set_frequency_sensor)
await sensors(CONF_POWER_FACTOR_1, var.set_power_factor_1_sensor)
await sensors(CONF_CURRENT_1, var.set_current_1_sensor)
await sensors(CONF_CURRENT_2, var.set_current_2_sensor)
await sensors(CONF_ACTIVE_POWER_1, var.set_active_power_1_sensor)
await sensors(CONF_ACTIVE_POWER_2, var.set_active_power_2_sensor)
await sensors(CONF_ENERGY_1, var.set_energy_1_sensor)
await sensors(CONF_ENERGY_2, var.set_energy_2_sensor)
+1 -4
View File
@@ -67,10 +67,7 @@ bool CSE7766Component::check_byte_() {
}
if (index == CSE7766_RAW_DATA_SIZE - 1) {
uint8_t checksum = 0;
for (uint8_t i = 2; i < CSE7766_RAW_DATA_SIZE - 1; i++) {
checksum += this->raw_data_[i];
}
const uint8_t checksum = sum_bytes(this->raw_data_ + 2, CSE7766_RAW_DATA_SIZE - 3);
if (checksum != this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]) {
ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum,
+107 -18
View File
@@ -1,25 +1,40 @@
#include "daikin.h"
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
namespace esphome::daikin {
ESPHOME_LOG_TAG(TAG, "daikin.climate");
static constexpr uint32_t BOOST_TIMEOUT_ID = 0;
void DaikinClimate::setup() {
climate_ir::ClimateIR::setup();
// A boost preset restored from flash gets its timer too
this->update_boost_timer_(false);
}
void DaikinClimate::control(const climate::ClimateCall &call) {
const bool was_boost = this->preset == climate::CLIMATE_PRESET_BOOST;
climate_ir::ClimateIR::control(call);
this->update_boost_timer_(was_boost);
}
void DaikinClimate::transmit_state() {
uint8_t remote_state[35] = {0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0xD7, 0x11, 0xDA, 0x27, 0x00,
uint8_t remote_state[35] = {0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0x00, 0x11, 0xDA, 0x27, 0x00,
0x42, 0x49, 0x05, 0xA2, 0x11, 0xDA, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00};
this->set_presets_(remote_state);
remote_state[21] = this->operation_mode_();
remote_state[22] = this->temperature_();
uint16_t fan_speed = this->fan_speed_();
remote_state[24] = fan_speed >> 8;
remote_state[25] = fan_speed & 0xff;
// Calculate checksum
for (int i = 16; i < 34; i++) {
remote_state[34] += remote_state[i];
}
remote_state[7] = sum_bytes(remote_state, 7);
remote_state[34] = sum_bytes(remote_state + 16, 18);
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
@@ -136,18 +151,52 @@ uint8_t DaikinClimate::temperature_() const {
case climate::CLIMATE_MODE_DRY:
return 0xc0;
default:
uint8_t temperature = (uint8_t) roundf(clamp<float>(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX));
return temperature << 1;
return static_cast<uint8_t>(
roundf(clamp<float>(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX) * 2.0f));
}
}
bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
uint8_t checksum = 0;
for (int i = 0; i < (DAIKIN_STATE_FRAME_SIZE - 1); i++) {
checksum += frame[i];
void DaikinClimate::set_presets_(uint8_t *remote_state) const {
switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) {
case climate::CLIMATE_PRESET_BOOST:
remote_state[29] = DAIKIN_PRESET_POWERFUL_ON;
break;
case climate::CLIMATE_PRESET_SLEEP:
remote_state[29] = DAIKIN_PRESET_QUIET_ON;
break;
case climate::CLIMATE_PRESET_ECO:
remote_state[32] = DAIKIN_PRESET_ECONO_ON;
break;
case climate::CLIMATE_PRESET_ACTIVITY:
remote_state[32] = DAIKIN_PRESET_SENSOR_ON;
break;
case climate::CLIMATE_PRESET_COMFORT:
remote_state[6] = DAIKIN_PRESET_COMFORT_ON; // the only preset carried by the first frame
break;
default:
break;
}
if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum)
return false;
}
void DaikinClimate::update_boost_timer_(bool was_boost) {
const bool is_boost = this->preset == climate::CLIMATE_PRESET_BOOST;
if (is_boost && !was_boost) {
this->set_timeout(BOOST_TIMEOUT_ID, DAIKIN_BOOST_DURATION_MS, [this]() { this->cancel_boost_mode_(); });
} else if (was_boost && !is_boost) {
this->cancel_timeout(BOOST_TIMEOUT_ID);
}
}
void DaikinClimate::cancel_boost_mode_() {
if (this->preset != climate::CLIMATE_PRESET_BOOST)
return;
ESP_LOGD(TAG, "Boost ended");
this->preset = climate::CLIMATE_PRESET_NONE;
this->publish_state();
}
bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
const bool was_boost = this->preset == climate::CLIMATE_PRESET_BOOST;
uint8_t mode = frame[5];
// Temperature is given in degrees celcius * 2
// only update for states that use the temperature
@@ -206,16 +255,42 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
this->fan_mode = climate::CLIMATE_FAN_QUIET;
break;
}
// Comfort lives in the first frame, recorded as pending until this frame proves the transmission valid
// A comfort frame whose own state frame never came must not attach to a later transmission
const uint32_t now = App.get_loop_component_start_time();
const PendingComfort comfort = now - this->pending_comfort_at_ <= DAIKIN_TRANSMISSION_MS
? this->pending_comfort_
: PendingComfort::PENDING_COMFORT_UNKNOWN;
this->pending_comfort_ = PendingComfort::PENDING_COMFORT_UNKNOWN;
if (frame[13] & DAIKIN_PRESET_POWERFUL_ON) {
this->preset = climate::CLIMATE_PRESET_BOOST;
} else if (frame[13] & DAIKIN_PRESET_QUIET_ON) {
this->preset = climate::CLIMATE_PRESET_SLEEP;
} else if (frame[16] & DAIKIN_PRESET_ECONO_ON) {
this->preset = climate::CLIMATE_PRESET_ECO;
} else if (frame[16] & DAIKIN_PRESET_SENSOR_ON) {
this->preset = climate::CLIMATE_PRESET_ACTIVITY;
} else if (comfort == PendingComfort::PENDING_COMFORT_ON) {
this->preset = climate::CLIMATE_PRESET_COMFORT;
} else if (comfort == PendingComfort::PENDING_COMFORT_OFF || this->preset != climate::CLIMATE_PRESET_COMFORT) {
// Without a comfort frame in this transmission a current comfort preset is left alone
this->preset = climate::CLIMATE_PRESET_NONE;
}
this->update_boost_timer_(was_boost);
this->publish_state();
return true;
}
bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE] = {};
uint8_t frame[DAIKIN_STATE_FRAME_SIZE] = {};
if (!data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
return false;
}
for (uint8_t pos = 0; pos < DAIKIN_STATE_FRAME_SIZE; pos++) {
uint8_t frame_size = DAIKIN_STATE_FRAME_SIZE;
for (uint8_t pos = 0; pos < frame_size; pos++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
@@ -224,7 +299,7 @@ bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
}
}
state_frame[pos] = byte;
frame[pos] = byte;
if (pos == 0) {
// frame header
if (byte != 0x11)
@@ -243,11 +318,25 @@ bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
} else if (pos == 4) {
// frame type
if (byte != 0x00)
if (byte == DAIKIN_FRAME_TYPE_COMFORT) {
frame_size = DAIKIN_COMFORT_FRAME_SIZE;
} else if (byte != DAIKIN_FRAME_TYPE_STATE) {
return false;
}
}
}
return this->parse_state_frame_(state_frame);
if (frame[frame_size - 1] != sum_bytes(frame, frame_size - 1)) {
return false;
}
if (frame_size == DAIKIN_COMFORT_FRAME_SIZE) {
// Nothing changes yet: the state frame that follows applies it once it has passed its own checksum
this->pending_comfort_ = (frame[6] & DAIKIN_PRESET_COMFORT_ON) ? PendingComfort::PENDING_COMFORT_ON
: PendingComfort::PENDING_COMFORT_OFF;
this->pending_comfort_at_ = App.get_loop_component_start_time();
return true;
}
return this->parse_state_frame_(frame);
}
} // namespace esphome::daikin
+34 -6
View File
@@ -27,6 +27,17 @@ const uint8_t DAIKIN_FAN_3 = 0x50;
const uint8_t DAIKIN_FAN_4 = 0x60;
const uint8_t DAIKIN_FAN_5 = 0x70;
// Presets: bit flags in byte 6 (comfort), byte 29 (powerful, quiet) and byte 32 (sensor, econo)
const uint8_t DAIKIN_PRESET_COMFORT_ON = 0x10;
const uint8_t DAIKIN_PRESET_POWERFUL_ON = 0x01;
const uint8_t DAIKIN_PRESET_QUIET_ON = 0x20;
const uint8_t DAIKIN_PRESET_ECONO_ON = 0x04;
const uint8_t DAIKIN_PRESET_SENSOR_ON = 0x02;
// The unit leaves powerful mode by itself after 20 minutes; mirror that in the entity
const uint32_t DAIKIN_BOOST_DURATION_MS = 20 * 60 * 1000;
// The three frames of one transmission arrive well inside this window
const uint32_t DAIKIN_TRANSMISSION_MS = 1000;
// IR Transmission
const uint32_t DAIKIN_IR_FREQUENCY = 38000;
const uint32_t DAIKIN_HEADER_MARK = 3360;
@@ -36,27 +47,44 @@ const uint32_t DAIKIN_ONE_SPACE = 1370;
const uint32_t DAIKIN_ZERO_SPACE = 360;
const uint32_t DAIKIN_MESSAGE_SPACE = 32300;
// State Frame size
// Frame types (byte 4) and sizes
const uint8_t DAIKIN_FRAME_TYPE_STATE = 0x00;
const uint8_t DAIKIN_FRAME_TYPE_COMFORT = 0xC5;
const uint8_t DAIKIN_STATE_FRAME_SIZE = 19;
const uint8_t DAIKIN_COMFORT_FRAME_SIZE = 8;
class DaikinClimate final : public climate_ir::ClimateIR {
public:
DaikinClimate()
: climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_QUIET, climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
: climate_ir::ClimateIR(
DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 0.5f, true, true,
{climate::CLIMATE_FAN_QUIET, climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_BOTH},
{climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_COMFORT,
climate::CLIMATE_PRESET_SLEEP, climate::CLIMATE_PRESET_ACTIVITY, climate::CLIMATE_PRESET_NONE}) {}
protected:
void setup() override;
void control(const climate::ClimateCall &call) override;
// Transmit via IR the state of this climate controller.
void transmit_state() override;
uint8_t operation_mode_() const;
uint16_t fan_speed_() const;
uint8_t temperature_() const;
void set_presets_(uint8_t *remote_state) const;
/// Arms or cancels the boost timer when the preset moves into or out of boost.
void update_boost_timer_(bool was_boost);
void cancel_boost_mode_();
// Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override;
bool parse_state_frame_(const uint8_t frame[]);
/// What the 0xC5 frame of the current transmission said about comfort, applied by the state frame
enum class PendingComfort : uint8_t { PENDING_COMFORT_UNKNOWN, PENDING_COMFORT_OFF, PENDING_COMFORT_ON };
PendingComfort pending_comfort_{PendingComfort::PENDING_COMFORT_UNKNOWN};
uint32_t pending_comfort_at_{0};
};
} // namespace esphome::daikin
+1 -4
View File
@@ -246,10 +246,7 @@ climate::ClimateTraits DaikinArcClimate::traits() {
}
bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) {
uint8_t checksum = 0;
for (int i = 0; i < (DAIKIN_STATE_FRAME_SIZE - 1); i++) {
checksum += frame[i];
}
const uint8_t checksum = sum_bytes(frame, DAIKIN_STATE_FRAME_SIZE - 1);
if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum) {
ESP_LOGI(TAG, "checksum error");
return false;
+2 -4
View File
@@ -1,5 +1,6 @@
#include "daikin_brc.h"
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/helpers.h"
namespace esphome::daikin_brc {
@@ -160,10 +161,7 @@ uint8_t DaikinBrcClimate::temperature_() {
}
bool DaikinBrcClimate::parse_state_frame_(const uint8_t frame[]) {
uint8_t checksum = 0;
for (int i = 0; i < (DAIKIN_BRC_STATE_FRAME_SIZE - 1); i++) {
checksum += frame[i];
}
const uint8_t checksum = sum_bytes(frame, DAIKIN_BRC_STATE_FRAME_SIZE - 1);
if (frame[DAIKIN_BRC_STATE_FRAME_SIZE - 1] != checksum) {
ESP_LOGCONFIG(TAG, "Bad CheckSum %x", checksum);
return false;
+1 -6
View File
@@ -129,12 +129,7 @@ void DalyBmsComponent::decode_data_(std::vector<uint8_t> data) {
while ((it = std::find(it, data.end(), 0xA5)) != data.end()) {
if (data.end() - it >= DALY_FRAME_SIZE && it[1] == 0x01) {
uint8_t checksum;
int sum = 0;
for (int i = 0; i < 12; i++) {
sum += it[i];
}
checksum = sum;
const uint8_t checksum = sum_bytes(&*it, 12);
if (checksum == it[12]) {
switch (it[2]) {
+2 -4
View File
@@ -1,5 +1,6 @@
#include "delonghi.h"
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/helpers.h"
namespace esphome::delonghi {
@@ -102,10 +103,7 @@ uint8_t DelonghiClimate::temperature_() {
}
bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
uint8_t checksum = 0;
for (int i = 0; i < (DELONGHI_STATE_FRAME_SIZE - 1); i++) {
checksum += frame[i];
}
const uint8_t checksum = sum_bytes(frame, DELONGHI_STATE_FRAME_SIZE - 1);
if (frame[DELONGHI_STATE_FRAME_SIZE - 1] != checksum) {
return false;
}
+14 -16
View File
@@ -21,7 +21,6 @@ from esphome.const import (
CONF_PAGES,
CONF_ROTATION,
CONF_TO,
CONF_TRIGGER_ID,
CONF_UPDATE_INTERVAL,
CONF_WIDTH,
SCHEDULER_DONT_RUN,
@@ -39,14 +38,13 @@ DisplayPage = display_ns.class_("DisplayPage")
DisplayPagePtr = DisplayPage.operator("ptr")
DisplayRef = Display.operator("ref")
DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action)
DisplayOnPageChangeTrigger = display_ns.class_(
"DisplayOnPageChangeTrigger", automation.Trigger
)
CONF_ON_PAGE_CHANGE = "on_page_change"
CONF_SHOW_TEST_CARD = "show_test_card"
CONF_UNSPECIFIED = "unspecified"
_request_page_change_slot = cg.slot_counter("DISPLAY_PAGE_CHANGE_CALLBACK_COUNT")
DISPLAY_ROTATIONS = {
0: display_ns.DISPLAY_ROTATION_0_DEGREES,
90: display_ns.DISPLAY_ROTATION_90_DEGREES,
@@ -99,9 +97,6 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
}
@@ -137,15 +132,18 @@ async def setup_display_core_(var, config):
pages.append(page)
cg.add(var.set_pages(pages))
for conf in config.get(CONF_ON_PAGE_CHANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_FROM in conf:
page = await cg.get_variable(conf[CONF_FROM])
cg.add(trigger.set_from(page))
if CONF_TO in conf:
page = await cg.get_variable(conf[CONF_TO])
cg.add(trigger.set_to(page))
await automation.build_automation(
trigger, [(DisplayPagePtr, "from"), (DisplayPagePtr, "to")], conf
_request_page_change_slot(str(var))
checks = []
for key, name in ((CONF_FROM, "from"), (CONF_TO, "to")):
if (page_id := conf.get(key)) is not None:
page = await cg.get_variable(page_id)
checks.append(f"{name} == ::{page}")
await automation.build_callback_automation(
var,
"add_on_page_change_callback",
[(DisplayPagePtr, "from"), (DisplayPagePtr, "to")],
conf,
when=" && ".join(checks) or None,
)
if config.get(CONF_SHOW_TEST_CARD):
cg.add(var.show_test_card())
+7 -10
View File
@@ -678,12 +678,14 @@ void Display::set_pages(std::vector<DisplayPage *> pages) {
}
void Display::show_page(DisplayPage *page) {
this->previous_page_ = this->page_;
#ifdef DISPLAY_PAGE_CHANGE_CALLBACK_COUNT
DisplayPage *previous = this->page_;
this->page_ = page;
if (this->previous_page_ != this->page_) {
for (auto *t : on_page_change_triggers_)
t->process(this->previous_page_, this->page_);
}
if (previous != page)
this->page_change_callback_.call(previous, page);
#else
this->page_ = page;
#endif
}
void Display::do_update_() {
@@ -700,11 +702,6 @@ void Display::do_update_() {
this->clear_clipping_();
}
void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
this->trigger(from, to);
}
void Display::strftime(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,
ESPTime time) {
char buffer[64];
+12 -18
View File
@@ -7,6 +7,8 @@
#include "esphome/core/color.h"
#include "esphome/core/automation.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/time.h"
#include "esphome/core/log.h"
#include "display_color_utils.h"
@@ -173,7 +175,6 @@ enum RegularPolygonDrawing {
class Display;
class DisplayPage;
class DisplayOnPageChangeTrigger;
/** Optimized display writer that uses function pointers for stateless lambdas.
*
@@ -294,7 +295,7 @@ using display_writer_t = DisplayWriter<Display>;
prefix type "\n" \
"%s Rotations: %d °\n" \
"%s Dimensions: %dpx x %dpx", \
prefix, log_display_obj->rotation_, prefix, log_display_obj->get_width(), \
prefix, log_display_obj->get_rotation(), prefix, log_display_obj->get_width(), \
log_display_obj->get_height()); \
}
@@ -702,7 +703,11 @@ class Display : public PollingComponent {
const DisplayPage *get_active_page() const { return this->page_; }
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
#ifdef DISPLAY_PAGE_CHANGE_CALLBACK_COUNT
template<typename F> void add_on_page_change_callback(F &&callback) {
this->page_change_callback_.add(std::forward<F>(callback));
}
#endif
/// Internal method to set the display rotation with.
virtual void set_rotation(DisplayRotation rotation);
@@ -710,7 +715,7 @@ class Display : public PollingComponent {
// Internal method to set display auto clearing.
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
DisplayRotation get_rotation() const { return this->rotation_; }
virtual DisplayRotation get_rotation() const { return this->rotation_; }
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
* returns the type the display is currently configured to.
@@ -809,8 +814,9 @@ class Display : public PollingComponent {
DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
display_writer_t writer_{};
DisplayPage *page_{nullptr};
DisplayPage *previous_page_{nullptr};
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
#ifdef DISPLAY_PAGE_CHANGE_CALLBACK_COUNT
StaticCallbackManager<DISPLAY_PAGE_CHANGE_CALLBACK_COUNT, void(DisplayPage *, DisplayPage *)> page_change_callback_;
#endif
bool auto_clear_enabled_{true};
std::vector<Rect> clipping_rectangle_;
uint8_t wdt_pixel_counter_{0};
@@ -850,18 +856,6 @@ template<typename... Ts> class DisplayPageShowAction final : public Action<Ts...
}
};
class DisplayOnPageChangeTrigger final : public Trigger<DisplayPage *, DisplayPage *> {
public:
explicit DisplayOnPageChangeTrigger(Display *parent) { parent->add_on_page_change_trigger(this); }
void process(DisplayPage *from, DisplayPage *to);
void set_from(DisplayPage *p) { this->from_ = p; }
void set_to(DisplayPage *p) { this->to_ = p; }
protected:
DisplayPage *from_{nullptr};
DisplayPage *to_{nullptr};
};
const LogString *text_align_to_string(TextAlign textalign);
} // namespace esphome::display
+24 -4
View File
@@ -44,13 +44,20 @@ def _get_data() -> EmonTxData:
return CORE.data[DOMAIN]
def _synchronous_automation_schema() -> cv.All:
# The received line and the parsed JSON document only live while the callbacks run.
return automation.validate_synchronous_automation(
"on_json and on_data", "the received data"
)
# Main configuration schema
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(EmonTx),
cv.Optional(CONF_ON_JSON): automation.validate_automation({}),
cv.Optional(CONF_ON_DATA): automation.validate_automation({}),
cv.Optional(CONF_ON_JSON): _synchronous_automation_schema(),
cv.Optional(CONF_ON_DATA): _synchronous_automation_schema(),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -104,10 +111,10 @@ _CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_JSON,
"add_on_json_callback",
[(cg.JsonObject, "json"), (cg.std_string, "raw_json")],
[(cg.JsonObject, "json"), (cg.StringRef, "raw_json")],
),
automation.CallbackAutomation(
CONF_ON_DATA, "add_on_data_callback", [(cg.std_string, "data")]
CONF_ON_DATA, "add_on_data_callback", [(cg.StringRef, "data")]
),
)
@@ -150,3 +157,16 @@ automation.register_apply_action(
CONF_COMMAND, "send_command", cg.std_string, const_fn=_plain_literal
),
)
# Let another component (e.g. serial_proxy) take exclusive ownership of the UART.
EMONTX_ID_SCHEMA = automation.maybe_simple_id({cv.GenerateID(): cv.use_id(EmonTx)})
automation.register_apply_action(
"emontx.pause", EMONTX_ID_SCHEMA, automation.ApplyCall("set_paused(true)")
)
automation.register_apply_action(
"emontx.resume", EMONTX_ID_SCHEMA, automation.ApplyCall("set_paused(false)")
)
automation.register_apply_condition("emontx.is_paused", EMONTX_ID_SCHEMA, "is_paused()")
+29 -6
View File
@@ -16,15 +16,31 @@ void EmonTx::setup() { this->buffer_pos_ = 0; }
* 2. If line starts with '{', parse as JSON and update sensors/callbacks
*/
void EmonTx::loop() {
if (this->is_paused()) {
this->was_paused_ = true;
return;
}
if (this->was_paused_) {
// Drop what arrived during the pause and resync at the next line start
this->was_paused_ = false;
this->buffer_pos_ = 0;
this->discarding_line_ = true;
uint8_t dropped;
while (this->available() > 0 && this->read_byte(&dropped))
this->discarding_line_ = dropped != '\n';
}
// Read all available data to prevent UART buffer overflow
while (this->available() > 0) {
uint8_t received = this->read();
uint8_t received;
if (!this->read_byte(&received))
break;
if (received == '\r') {
continue; // Ignore CR
} else if (received == '\n') {
if (received == '\n') {
// End of line - process the buffer
if (this->buffer_pos_ > 0) {
if (this->discarding_line_) {
this->discarding_line_ = false;
} else if (this->buffer_pos_ > 0) {
// Null-terminate for safe logging and c_str() use
size_t len = this->buffer_pos_;
this->buffer_[len] = '\0';
@@ -42,9 +58,12 @@ void EmonTx::loop() {
this->parse_json_(this->buffer_.data(), len);
}
}
} else if (received == '\r' || this->discarding_line_) {
// Ignore CR, and the rest of a line that overflowed or was interrupted by a pause
} else if (this->buffer_pos_ >= MAX_LINE_LENGTH) {
ESP_LOGW(TAG, "Buffer overflow (>%zu bytes), discarding buffer", MAX_LINE_LENGTH);
ESP_LOGW(TAG, "Buffer overflow (>%zu bytes), discarding line", MAX_LINE_LENGTH);
this->buffer_pos_ = 0;
this->discarding_line_ = true;
} else {
this->buffer_[this->buffer_pos_++] = static_cast<char>(received);
}
@@ -95,6 +114,10 @@ void EmonTx::dump_config() {
* @param command The command string to send (LF will be appended automatically).
*/
void EmonTx::send_command(const char *command) {
if (this->is_paused()) {
ESP_LOGW(TAG, "Not sending command, UART is paused: %s", command);
return;
}
ESP_LOGD(TAG, "Sending command to emonTx: %s", command);
this->write_str(command);
this->write_byte('\n');
+10
View File
@@ -8,6 +8,7 @@
#include "esphome/components/json/json_util.h"
#include <array>
#include <atomic>
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
@@ -41,6 +42,12 @@ class EmonTx final : public Component, public uart::UARTDevice {
void send_command(const char *command);
void send_command(const std::string &command) { this->send_command(command.c_str()); }
/// Stop reading the UART and sending commands, so another component can use it.
/// Atomic, so it can be called from any task. Hold the pause for at least one loop() pass.
void set_paused(bool paused) { this->paused_.store(paused, std::memory_order_relaxed); }
bool is_paused() const { return this->paused_.load(std::memory_order_relaxed); }
#ifdef USE_SENSOR
void init_sensors(size_t count) { this->sensors_.init(count); }
void register_sensor(const char *tag_name, sensor::Sensor *sensor);
@@ -55,6 +62,9 @@ class EmonTx final : public Component, public uart::UARTDevice {
LazyCallbackManager<void(JsonObject, StringRef)> json_callbacks_;
LazyCallbackManager<void(StringRef)> data_callbacks_;
uint16_t buffer_pos_{0};
std::atomic<bool> paused_{false};
bool was_paused_{false};
bool discarding_line_{false}; // Set after an overflow or a pause until the end of that line
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
};
+1 -1
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.const import CONF_AQI
import esphome.config_validation as cv
from esphome.const import (
CONF_COMPENSATION,
@@ -26,7 +27,6 @@ DOMAIN = "ens160_base"
ens160_ns = cg.esphome_ns.namespace("ens160_base")
CONF_AQI = "aqi"
CONFIG_SCHEMA_BASE = cv.Schema(
{
+98 -3
View File
@@ -551,9 +551,88 @@ assert all(variant in CPU_FREQUENCIES for variant in VARIANTS)
FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values()))
_SDKCONFIG_CPU_FREQUENCY_OPTION = "CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ"
_SDKCONFIG_CPU_FREQUENCY_PREFIX = f"{_SDKCONFIG_CPU_FREQUENCY_OPTION}_"
# Older IDF versions named the CPU frequency options after the chip
_LEGACY_SDKCONFIG_CPU_FREQUENCY_VARIANTS = frozenset(
{VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3}
)
def _get_sdkconfig_cpu_frequencies(
sdkconfig_options: dict[str, str], variant: str
) -> tuple[set[int], set[int]]:
"""Return the CPU frequencies in MHz that sdkconfig_options selects and disables."""
# (integer option, choice option prefix) pairs that can carry a frequency
keys = [(_SDKCONFIG_CPU_FREQUENCY_OPTION, _SDKCONFIG_CPU_FREQUENCY_PREFIX)]
if variant in _LEGACY_SDKCONFIG_CPU_FREQUENCY_VARIANTS:
keys.append(
(
f"CONFIG_{variant}_DEFAULT_CPU_FREQ_MHZ",
f"CONFIG_{variant}_DEFAULT_CPU_FREQ_",
)
)
selected: set[int] = set()
disabled: set[int] = set()
for integer_key, choice_prefix in keys:
if (value := sdkconfig_options.get(integer_key)) is not None:
try:
selected.add(int(value))
except ValueError as err:
raise cv.Invalid(
f"{integer_key} must be an integer MHz value",
path=[CONF_FRAMEWORK, CONF_SDKCONFIG_OPTIONS],
) from err
for name, value in sdkconfig_options.items():
mhz = name.removeprefix(choice_prefix)
if mhz == name or not mhz.isdigit():
continue
if value.lower() == "y":
selected.add(int(mhz))
elif value.lower() == "n":
disabled.add(int(mhz))
else:
raise cv.Invalid(
f"{name} must be y or n",
path=[CONF_FRAMEWORK, CONF_SDKCONFIG_OPTIONS],
)
return selected, disabled
def set_core_data(config):
cpu_frequency = config.get(CONF_CPU_FREQUENCY, None)
cpu_frequency = config.get(CONF_CPU_FREQUENCY)
variant = config[CONF_VARIANT]
sdkconfig_path = [CONF_FRAMEWORK, CONF_SDKCONFIG_OPTIONS]
selected, disabled = _get_sdkconfig_cpu_frequencies(
config[CONF_FRAMEWORK][CONF_SDKCONFIG_OPTIONS], variant
)
if len(selected) > 1:
conflicting = ", ".join(f"{mhz}MHz" for mhz in sorted(selected))
raise cv.Invalid(
f"sdkconfig_options contains conflicting CPU frequencies ({conflicting})",
path=sdkconfig_path,
)
sdkconfig_cpu_frequency = f"{selected.pop()}MHZ" if selected else None
if cpu_frequency is None and sdkconfig_cpu_frequency is not None:
if sdkconfig_cpu_frequency not in CPU_FREQUENCIES[variant]:
raise cv.Invalid(
f"sdkconfig_options selects {sdkconfig_cpu_frequency}, which {variant} does not support",
path=sdkconfig_path,
)
_LOGGER.warning(
"sdkconfig_options contains a CPU frequency setting; using %s. "
"Set 'esp32.cpu_frequency' to configure it directly.",
sdkconfig_cpu_frequency,
)
cpu_frequency = sdkconfig_cpu_frequency
elif (
sdkconfig_cpu_frequency is not None and sdkconfig_cpu_frequency != cpu_frequency
):
raise cv.Invalid(
f"esp32.cpu_frequency ({cpu_frequency}) conflicts with sdkconfig_options "
f"({sdkconfig_cpu_frequency})",
path=sdkconfig_path,
)
# if not specified in config, default to the maximum supported frequency
# (ESP32-P4 engineering samples are limited to 360MHz, non-engineering can do 400MHz)
if cpu_frequency is None:
@@ -562,12 +641,17 @@ def set_core_data(config):
cpu_frequency = "360MHZ"
else:
cpu_frequency = choices[-1]
config[CONF_CPU_FREQUENCY] = cpu_frequency
elif cpu_frequency not in CPU_FREQUENCIES[variant]:
raise cv.Invalid(
f"Invalid CPU frequency '{cpu_frequency}' for {config[CONF_VARIANT]}",
path=[CONF_CPU_FREQUENCY],
)
if int(cpu_frequency[:-3]) in disabled:
raise cv.Invalid(
f"sdkconfig_options disables the selected CPU frequency ({cpu_frequency})",
path=sdkconfig_path,
)
config[CONF_CPU_FREQUENCY] = cpu_frequency
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
_LOGGER.warning(
@@ -1987,6 +2071,17 @@ def require_vfs_termios() -> None:
CORE.data[KEY_VFS_TERMIOS_REQUIRED] = True
def allow_insecure_tls() -> None:
"""Let esp-tls connect without verifying the server certificate.
The two sdkconfig flags are global, so a component asking for an unverified
connection switches them on for the whole build; they are never switched off
here because another component may have asked first.
"""
add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
add_idf_sdkconfig_option("CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True)
def require_certificate_bundle() -> None:
"""Enable the mbedTLS root certificate bundle for this build.
@@ -3138,7 +3233,7 @@ async def to_code(config):
# Set default CPU frequency
add_idf_sdkconfig_option(
f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True
f"{_SDKCONFIG_CPU_FREQUENCY_PREFIX}{config[CONF_CPU_FREQUENCY][:-3]}", True
)
# Apply LWIP optimization settings
+53 -15
View File
@@ -1,4 +1,6 @@
import encodings
import struct
from typing import Any
from esphome import automation
import esphome.codegen as cg
@@ -25,8 +27,10 @@ from esphome.const import (
__version__ as ESPHOME_VERSION,
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
import esphome.final_validate as fv
from esphome.schema_extractors import SCHEMA_EXTRACT
from esphome.types import ConfigType, TemplateArgsType
AUTO_LOAD = ["esp32_ble", "bytebuffer"]
CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"]
@@ -111,8 +115,6 @@ BLECharacteristicNotifyAction = esp32_ble_server_automations_ns.class_(
)
bytebuffer_ns = cg.esphome_ns.namespace("bytebuffer")
Endianness_ns = bytebuffer_ns.namespace("Endian")
ByteBuffer_ns = bytebuffer_ns.namespace("ByteBuffer")
ByteBuffer = bytebuffer_ns.class_("ByteBuffer")
PROPERTY_MAP = {
@@ -141,6 +143,15 @@ class ValueType:
return value
def float32(value: Any) -> float:
value = cv.float_(value)
try:
struct.pack("<f", value)
except OverflowError as e:
raise cv.Invalid(f"{value} is out of range for a float") from e
return value
VALUE_TYPES = {
type_name: ValueType(type_name, validator, length)
for type_name, validator, length in (
@@ -152,7 +163,7 @@ VALUE_TYPES = {
("int16_t", cv.int_range(-32768, 32767), 2),
("int32_t", cv.int_range(-2147483648, 2147483647), 4),
("int64_t", cv.int_range(-9223372036854775808, 9223372036854775807), 8),
("float", cv.float_, 4),
("float", float32, 4),
("double", cv.float_, 8),
("string", cv.string_strict, None), # Length is variable
)
@@ -495,19 +506,48 @@ def parse_uuid(uuid):
return ESPBTUUID_ns.from_raw(uuid)
_STRUCT_FORMATS = {
"uint8_t": "B",
"uint16_t": "H",
"uint32_t": "I",
"uint64_t": "Q",
"int8_t": "b",
"int16_t": "h",
"int32_t": "i",
"int64_t": "q",
"float": "f",
"double": "d",
}
def value_bytes(value_config: ConfigType) -> list[int]:
"""The bytes of a constant value, as ByteBuffer::wrap packed them at runtime."""
value = value_config[CONF_DATA]
if isinstance(value, str):
return list(value.encode(value_config[CONF_STRING_ENCODING]))
if isinstance(value, list):
return value
order = ">" if value_config[CONF_ENDIANNESS] == "BIG" else "<"
return list(struct.pack(order + _STRUCT_FORMATS[value_config[CONF_TYPE]], value))
async def parse_value(value_config, args):
value = value_config[CONF_DATA]
if isinstance(value, cv.Lambda):
return await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
# An initializer list calls the set_value(std::initializer_list<uint8_t>) overload
return cg.ArrayInitializer(*value_bytes(value_config))
if isinstance(value, str):
value = list(value.encode(value_config[CONF_STRING_ENCODING]))
if isinstance(value, list):
# Generate initializer list {1, 2, 3} instead of std::vector<uint8_t>({1, 2, 3})
# This calls the set_value(std::initializer_list<uint8_t>) overload
return cg.ArrayInitializer(*value)
val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])
async def set_value_buffer(
var: MockObj, value_config: ConfigType, args: TemplateArgsType
) -> None:
value = value_config[CONF_DATA]
if not isinstance(value, cv.Lambda):
value = value_bytes(value_config)
await automation.templatable_bytes(
value, args, var.set_buffer_template, var.set_buffer_static, "ble_server_value"
)
def calculate_num_handles(service_config):
@@ -662,8 +702,7 @@ async def to_code(config):
async def ble_server_characteristic_set_value(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
value = await parse_value(config[CONF_VALUE], args)
cg.add(var.set_buffer(value))
await set_value_buffer(var, config[CONF_VALUE], args)
cg.add_define("USE_ESP32_BLE_SERVER_SET_VALUE_ACTION")
return var
@@ -682,8 +721,7 @@ async def ble_server_characteristic_set_value(config, action_id, template_arg, a
async def ble_server_descriptor_set_value(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
value = await parse_value(config[CONF_VALUE], args)
cg.add(var.set_buffer(value))
await set_value_buffer(var, config[CONF_VALUE], args)
cg.add_define("USE_ESP32_BLE_SERVER_DESCRIPTOR_SET_VALUE_ACTION")
return var
@@ -30,6 +30,14 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void BLECharacteristic::set_value(const uint8_t *data, size_t length) {
if (length == 0) { // An empty constant is a nullptr table
this->value_.clear();
return;
}
this->value_.assign(data, data + length);
}
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { this->value_ = std::move(buffer); }
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
@@ -30,6 +30,7 @@ class BLECharacteristic final {
~BLECharacteristic();
void set_value(ByteBuffer buffer);
void set_value(const uint8_t *data, size_t length);
void set_value(std::vector<uint8_t> &&buffer);
void set_value(std::initializer_list<uint8_t> data);
void set_value(const std::string &buffer);
@@ -49,17 +49,18 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
this->state_ = CREATING;
}
void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); }
void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value(buffer.data(), buffer.size()); }
void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value_impl_(data.begin(), data.size()); }
void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value(data.begin(), data.size()); }
void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) {
void BLEDescriptor::set_value(const uint8_t *data, size_t length) {
if (length > this->value_.attr_max_len) {
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
return;
}
this->value_.attr_len = length;
memcpy(this->value_.attr_value, data, length);
if (length != 0) // An empty constant is a nullptr table
memcpy(this->value_.attr_value, data, length);
}
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
@@ -26,6 +26,7 @@ class BLEDescriptor {
void do_create(BLECharacteristic *characteristic);
ESPBTUUID get_uuid() const { return this->uuid_; }
void set_value(const uint8_t *data, size_t length);
void set_value(std::vector<uint8_t> &&buffer);
void set_value(std::initializer_list<uint8_t> data);
void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
@@ -42,8 +43,6 @@ class BLEDescriptor {
}
protected:
void set_value_impl_(const uint8_t *data, size_t length);
BLECharacteristic *characteristic_{nullptr};
ESPBTUUID uuid_;
uint16_t handle_{0xFFFF};
@@ -16,6 +16,16 @@ namespace esphome::esp32_ble_server::esp32_ble_server_automations {
using namespace esp32_ble;
/// Sets a characteristic or descriptor value from a constant flash table or a lambda's vector.
// Not TemplatableBytes::visit, which would copy the lambda's vector instead of moving it into the value.
template<typename T, typename V, typename... Xs> void apply_value(T *target, const V &value, Xs &&...x) {
if (value.is_static()) {
target->set_value(value.data(), value.size());
} else {
target->set_value(value.value(x...));
}
}
class BLETriggers {
public:
#ifdef USE_ESP32_BLE_SERVER_CHARACTERISTIC_ON_WRITE
@@ -67,25 +77,23 @@ class BLECharacteristicSetValueActionManager {
template<typename... Ts> class BLECharacteristicSetValueAction final : public Action<Ts...> {
public:
BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); }
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
TEMPLATABLE_BYTES(buffer)
void play(const Ts &...x) override {
// If the listener is already set, do nothing
if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_))
return;
// Set initial value
this->parent_->set_value(this->buffer_.value(x...));
apply_value(this->parent_, this->buffer_, x...);
// Set the listener for read events
// ``mutable`` keeps by-copy captures non-const for triggers passing args by reference
// (e.g. climate on_control's ClimateCall&). See #17142.
this->parent_->on_read([this, x...](uint16_t id) mutable {
// Set the value of the characteristic every time it is read
this->parent_->set_value(this->buffer_.value(x...));
apply_value(this->parent_, this->buffer_, x...);
});
// Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic
BLECharacteristicSetValueActionManager::get_instance()->set_listener(
this->parent_, [this, x...]() mutable { this->parent_->set_value(this->buffer_.value(x...)); });
this->parent_, [this, x...]() mutable { apply_value(this->parent_, this->buffer_, x...); });
}
protected:
@@ -115,10 +123,8 @@ template<typename... Ts> class BLECharacteristicNotifyAction final : public Acti
template<typename... Ts> class BLEDescriptorSetValueAction final : public Action<Ts...> {
public:
BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); }
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); }
TEMPLATABLE_BYTES(buffer)
void play(const Ts &...x) override { apply_value(this->parent_, this->buffer_, x...); }
protected:
BLEDescriptor *parent_;
+1 -1
View File
@@ -129,7 +129,7 @@ async def to_code(config: ConfigType) -> None:
# Also enable esp_driver_twai for future migration to new API
include_builtin_idf_component("esp_driver_twai")
var = cg.new_Pvariable(config[CONF_ID])
await canbus.register_canbus(var, config)
await canbus.register_canbus(var, config, reports_status=True)
cg.add(var.set_rx(config[CONF_RX_PIN]))
cg.add(var.set_tx(config[CONF_TX_PIN]))
@@ -84,6 +84,8 @@ bool ESP32Can::setup_internal() {
twai_general_config_t g_config =
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, twai_mode);
g_config.controller_id = next_twai_ctrl_num++;
g_config.alerts_enabled = TWAI_ALERT_ERR_PASS | TWAI_ALERT_ERR_ACTIVE | TWAI_ALERT_BUS_OFF |
TWAI_ALERT_BUS_RECOVERED | TWAI_ALERT_RX_QUEUE_FULL;
if (this->tx_queue_len_.has_value()) {
g_config.tx_queue_len = this->tx_queue_len_.value();
}
@@ -116,6 +118,86 @@ bool ESP32Can::setup_internal() {
return true;
}
canbus::CanEventFlags ESP32Can::get_events() {
if (this->twai_handle_ == nullptr) {
return {}; // not set up yet, or setup failed
}
uint32_t alerts;
esp_err_t err = twai_read_alerts_v2(this->twai_handle_, &alerts, 0);
if (err != ESP_OK) {
if (err != ESP_ERR_TIMEOUT) {
ESP_LOGD(TAG, "failed to get CAN events");
}
alerts = 0;
}
uint8_t events = 0;
if (alerts & TWAI_ALERT_ERR_PASS) {
events |= canbus::CAN_EVENT_PASSIVE;
}
if (alerts & TWAI_ALERT_ERR_ACTIVE) {
events |= canbus::CAN_EVENT_ACTIVE;
}
if (alerts & TWAI_ALERT_BUS_OFF) {
events |= canbus::CAN_EVENT_BUS_OFF;
// Start recovery at once, as the MCP2515 does in hardware
this->recovery_pending_ = true;
}
if (alerts & TWAI_ALERT_BUS_RECOVERED) {
// Recovery leaves the controller stopped
this->restart_pending_ = true;
}
if (this->recovery_pending_ || this->restart_pending_) {
this->handle_bus_off_recovery_();
}
if (alerts & TWAI_ALERT_RX_QUEUE_FULL) {
events |= canbus::CAN_EVENT_RX_QUEUE_FULL;
}
return static_cast<canbus::CanEventFlags>(events);
}
void ESP32Can::handle_bus_off_recovery_() {
// These only fail with an invalid handle or outside the expected state; retry on the next pass
if (this->recovery_pending_) {
if (twai_initiate_recovery_v2(this->twai_handle_) != ESP_OK) {
this->status_set_error(LOG_STR("Recovery after bus off failed"));
return;
}
this->recovery_pending_ = false;
}
if (this->restart_pending_) {
if (twai_start_v2(this->twai_handle_) != ESP_OK) {
this->status_set_error(LOG_STR("Restart after bus off failed"));
return;
}
this->restart_pending_ = false;
}
this->status_clear_error();
}
canbus::CanStatus ESP32Can::get_status() {
canbus::CanStatus status = {};
twai_status_info_t twai_status;
if (this->twai_handle_ != nullptr && twai_get_status_info_v2(this->twai_handle_, &twai_status) == ESP_OK) {
status.bus_off = (twai_status.state == TWAI_STATE_BUS_OFF) || (twai_status.state == TWAI_STATE_RECOVERING);
status.rx_error_counter = twai_status.rx_error_counter;
status.tx_error_counter = twai_status.tx_error_counter;
status.tx_failed_count = twai_status.tx_failed_count;
status.rx_missed_count = twai_status.rx_missed_count;
status.rx_overrun_count = twai_status.rx_overrun_count;
status.arb_lost_count = twai_status.arb_lost_count;
status.bus_error_count = twai_status.bus_error_count;
} else {
ESP_LOGD(TAG, "failed to get CAN status");
}
return status;
}
canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
// In listen-only mode, we cannot transmit
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
+5
View File
@@ -24,16 +24,21 @@ class ESP32Can final : public canbus::Canbus {
void set_tx_enqueue_timeout_ms(uint32_t tx_enqueue_timeout_ms) {
this->tx_enqueue_timeout_ticks_ = pdMS_TO_TICKS(tx_enqueue_timeout_ms);
}
canbus::CanStatus get_status() override;
ESP32Can(){};
protected:
bool setup_internal() override;
canbus::CanEventFlags get_events() override;
canbus::Error send_message(struct canbus::CanFrame *frame) override;
canbus::Error read_message(struct canbus::CanFrame *frame) override;
void handle_bus_off_recovery_();
int rx_{-1};
int tx_{-1};
CanMode mode_{CAN_MODE_NORMAL};
bool recovery_pending_{false};
bool restart_pending_{false};
TickType_t tx_enqueue_timeout_ticks_{};
optional<uint32_t> tx_queue_len_{};
optional<uint32_t> rx_queue_len_{};
+24 -7
View File
@@ -17,6 +17,8 @@ from esphome.const import (
CONF_ON_ERROR,
CONF_TRIGGER_ID,
CONF_WIFI,
PLATFORM_ESP32,
PLATFORM_ESP8266,
)
from esphome.core import CORE, HexInt
from esphome.cpp_generator import MockObj, TemplateArgsType
@@ -24,7 +26,14 @@ import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"]
AUTO_LOAD = ["network"]
def AUTO_LOAD() -> list[str]:
# network initialises esp_netif and the default event loop on the ESP32. The ESP8266 needs neither, and
# loading it there would pull in mdns, which requires wifi
return ["network"] if CORE.is_esp32 else []
DOMAIN = "espnow"
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
@@ -121,15 +130,21 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ADDRESS): cv.mac_address,
}
),
cv.Optional(CONF_ON_BROADCAST): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastTrigger),
cv.Optional(CONF_ADDRESS): cv.mac_address,
}
# The ESP8266 SDK does not report the destination address, so broadcasts are indistinguishable there
cv.Optional(CONF_ON_BROADCAST): cv.All(
cv.only_on_esp32,
automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
OnBroadcastTrigger
),
cv.Optional(CONF_ADDRESS): cv.mac_address,
}
),
),
},
).extend(cv.COMPONENT_SCHEMA),
cv.only_on_esp32,
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
)
@@ -138,6 +153,8 @@ def _validate_variant(config: ConfigType) -> ConfigType:
# ESP-NOW; only the ESP32-P4 has a path, via the esp32_hosted shim that
# supplies the esp_now_* symbols. Fail here with a clear message instead of
# letting the build reach an "undefined reference to esp_now_*" link error.
if not CORE.is_esp32:
return config
variant = get_esp32_variant()
if wifi.variant_has_wifi(variant):
return config
+2 -2
View File
@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "espnow_component.h"
@@ -142,4 +142,4 @@ class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const ui
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+67 -100
View File
@@ -1,25 +1,19 @@
#include "espnow_component.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "espnow_err.h"
#include "espnow_platform.h"
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <utility>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <esp_event.h>
#include <esp_mac.h>
#include <esp_netif.h>
#include <esp_now.h>
#include <esp_random.h>
#include <esp_wifi.h>
#include <cstring>
#include <memory>
#ifdef USE_WIFI
#include "esphome/components/wifi/wifi_component.h"
#endif
@@ -49,7 +43,7 @@ static const LogString *espnow_error_to_str(esp_err_t error) {
case ESP_ERR_ESPNOW_INTERNAL:
return LOG_STR("Internal Error");
case ESP_ERR_ESPNOW_NO_MEM:
return LOG_STR("Our of memory");
return LOG_STR("Out of memory");
case ESP_ERR_ESPNOW_NOT_FOUND:
return LOG_STR("Peer not found");
case ESP_ERR_ESPNOW_IF:
@@ -57,96 +51,91 @@ static const LogString *espnow_error_to_str(esp_err_t error) {
case ESP_OK:
return LOG_STR("OK");
case ESP_NOW_SEND_FAIL:
case ESP_FAIL:
return LOG_STR("Failed");
default:
return LOG_STR("Unknown Error");
}
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status)
#else
void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
#endif
{
// The analyzer traces a leak on the failed-push path, which cannot happen: the
// pools are sized to the queue capacity (SIZE-1), so allocate() returns nullptr
// before push() can find the ring full.
// NOLINTBEGIN(clang-analyzer-unix.Malloc)
void ESPNowComponent::send_reported(const uint8_t *mac_addr, esp_now_send_status_t status) {
// Allocate an event from the pool
ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate();
ESPNowPacket *packet = this->receive_packet_pool_.allocate();
if (packet == nullptr) {
// No events available - queue is full or we're out of memory
global_esp_now->receive_packet_queue_.increment_dropped_count();
global_esp_now->enable_loop_soon_any_context();
this->receive_packet_queue_.increment_dropped_count();
this->enable_loop_soon_any_context();
return;
}
// Load new packet data (replaces previous packet)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
packet->load_sent_data(info->des_addr, status);
#else
packet->load_sent_data(mac_addr, status);
#endif
// Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet);
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
this->receive_packet_queue_.push(packet);
// Re-enable and wake the main loop to process the ESP-NOW send event
global_esp_now->enable_loop_soon_any_context();
this->enable_loop_soon_any_context();
}
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
void ESPNowComponent::packet_received(const uint8_t *src_addr, const uint8_t *des_addr, const uint8_t *data, int size,
int8_t rssi, uint32_t timestamp) {
// Drop oversized frames before copying. ESP-NOW v2 peers (IDF >= 5.4 builds a
// v2 stack with no opt-out) can send up to ESP_NOW_MAX_DATA_LEN_V2 (1470 B),
// but the receive buffer only fits v2 frames with ``max_payload_size``; copying a
// but the receive buffer only fits frames up to ``max_payload_size``; copying a
// larger frame would overflow packet_.receive.data.
if (size < 0 || size > ESPNOW_MAX_DATA_LEN) {
global_esp_now->receive_packet_queue_.increment_dropped_count();
global_esp_now->enable_loop_soon_any_context();
if (size < 0 || static_cast<size_t>(size) > ESPNOW_MAX_DATA_LEN) {
this->receive_packet_queue_.increment_dropped_count();
this->enable_loop_soon_any_context();
return;
}
// Allocate an event from the pool
ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate();
ESPNowPacket *packet = this->receive_packet_pool_.allocate();
if (packet == nullptr) {
// No events available - queue is full or we're out of memory
global_esp_now->receive_packet_queue_.increment_dropped_count();
global_esp_now->enable_loop_soon_any_context();
this->receive_packet_queue_.increment_dropped_count();
this->enable_loop_soon_any_context();
return;
}
// Load new packet data (replaces previous packet)
packet->load_received_data(info, data, size);
packet->load_received_data(src_addr, des_addr, data, size, rssi, timestamp);
// Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet);
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
this->receive_packet_queue_.push(packet);
// Re-enable and wake the main loop to process the ESP-NOW receive event
global_esp_now->enable_loop_soon_any_context();
this->enable_loop_soon_any_context();
}
// NOLINTEND(clang-analyzer-unix.Malloc)
ESPNowComponent::ESPNowComponent() { global_esp_now = this; }
void ESPNowComponent::dump_config() {
ESP_LOGCONFIG(TAG, "espnow:");
// Only report driver details once enabled; with enable_on_boot: false the
// Wi-Fi driver is not initialized yet and esp_now_get_version() would crash,
// Wi-Fi driver is not initialized yet and the version call would crash,
// and after a failed enable_() the values would be meaningless.
if (this->state_ != ESPNOW_STATE_ENABLED) {
// OFF here means enable_() failed; the core logs the FAILED marker separately
ESP_LOGCONFIG(TAG, " %s", this->is_disabled() ? LOG_STR_LITERAL("Disabled") : LOG_STR_LITERAL("Not enabled"));
return;
}
uint32_t version = 0;
esp_now_get_version(&version);
char own_addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(this->own_address_, own_addr_buf);
ESP_LOGCONFIG(TAG,
" Own address: %s\n"
" Version: v%" PRIu32 "\n"
" Wi-Fi channel: %d",
own_addr_buf, version, this->wifi_channel_);
own_addr_buf, this->wifi_channel_);
uint32_t version = platform::get_version();
if (version != 0) {
ESP_LOGCONFIG(TAG, " Version: v%" PRIu32, version);
}
#ifdef USE_WIFI
ESP_LOGCONFIG(TAG, " Wi-Fi enabled: %s", YESNO(this->is_wifi_enabled()));
#endif
@@ -183,6 +172,10 @@ void ESPNowComponent::on_wifi_connect_state(StringRef ssid, std::span<const uint
if (this->wifi_channel_ != old_channel) {
ESP_LOGI(TAG, "WiFi channel changed from %d to %d", old_channel, this->wifi_channel_);
}
// Peers added while the station was scanning were bound to the channel of that moment, so rebind on every connect
if (this->state_ == ESPNOW_STATE_ENABLED) {
platform::rebind_peers();
}
}
#endif
@@ -198,41 +191,19 @@ void ESPNowComponent::enable() {
void ESPNowComponent::enable_() {
if (!this->is_wifi_enabled()) {
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_disconnect());
platform::init_radio();
this->apply_wifi_channel();
}
this->get_wifi_channel();
esp_err_t err = esp_now_init();
esp_err_t err = platform::init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_init failed: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "Init failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
err = esp_now_register_recv_cb(on_data_received);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
err = esp_now_register_send_cb(on_send_report);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
esp_wifi_get_mac(WIFI_IF_STA, this->own_address_);
platform::read_mac(this->own_address_);
this->state_ = ESPNOW_STATE_ENABLED;
@@ -248,12 +219,9 @@ void ESPNowComponent::disable() {
ESP_LOGD(TAG, "Disabling");
this->state_ = ESPNOW_STATE_DISABLED;
esp_now_unregister_recv_cb();
esp_now_unregister_send_cb();
esp_err_t err = esp_now_deinit();
esp_err_t err = platform::deinit();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_deinit failed! 0x%x", err);
ESP_LOGE(TAG, "Deinit failed: %s", esp_err_to_name(err));
}
}
@@ -271,9 +239,10 @@ void ESPNowComponent::apply_wifi_channel() {
}
ESP_LOGI(TAG, "Channel set to %d.", this->wifi_channel_);
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(this->wifi_channel_, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
platform::set_channel(this->wifi_channel_);
if (this->state_ == ESPNOW_STATE_ENABLED) {
platform::rebind_peers();
}
}
void ESPNowComponent::loop() {
@@ -283,7 +252,7 @@ void ESPNowComponent::loop() {
switch (packet->type_) {
case ESPNowPacket::RECEIVED: {
const ESPNowRecvInfo info = packet->get_receive_info();
if (!esp_now_is_peer_exist(info.src_addr)) {
if (!platform::peer_exists(info.src_addr)) {
bool handled = false;
for (auto *handler : this->unknown_peer_handlers_) {
if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size)) {
@@ -296,7 +265,7 @@ void ESPNowComponent::loop() {
}
}
// Intentionally left as if instead of else in case the peer is added above
if (esp_now_is_peer_exist(info.src_addr)) {
if (platform::peer_exists(info.src_addr)) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
@@ -309,7 +278,8 @@ void ESPNowComponent::loop() {
format_hex_pretty_to(hex_buf, packet->packet_.receive.data,
std::min<uint16_t>(packet->packet_.receive.size, ESP_NOW_MAX_DATA_LEN)));
#endif
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
// Without a destination address every packet goes to the receive handlers
if (ESPNOW_REPORTS_DESTINATION && memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
for (auto *handler : this->broadcast_handlers_) {
if (handler->on_broadcast(info, packet->packet_.receive.data, packet->packet_.receive.size))
break; // If a handler returns true, stop processing further handlers
@@ -374,24 +344,25 @@ void ESPNowComponent::loop() {
}
uint8_t ESPNowComponent::get_wifi_channel() {
wifi_second_chan_t dummy;
esp_wifi_get_channel(&this->wifi_channel_, &dummy);
this->wifi_channel_ = platform::get_channel();
return this->wifi_channel_;
}
// Same analyzer false positive as above: the send pool is sized to the send queue's capacity
// NOLINTBEGIN(clang-analyzer-unix.Malloc)
esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
const send_callback_t &callback) {
send_callback_t callback) {
if (this->state_ != ESPNOW_STATE_ENABLED) {
return ESP_ERR_ESPNOW_NOT_INIT;
} else if (this->is_failed()) {
return ESP_ERR_ESPNOW_FAILED;
} else if (peer_address == 0ULL) {
} else if (peer_address == nullptr) {
return ESP_ERR_ESPNOW_PEER_NOT_SET;
} else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
return ESP_ERR_ESPNOW_OWN_ADDRESS;
} else if (size > ESPNOW_MAX_DATA_LEN) {
return ESP_ERR_ESPNOW_DATA_SIZE;
} else if (!esp_now_is_peer_exist(peer_address)) {
} else if (!platform::peer_exists(peer_address)) {
if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) {
esp_err_t err = this->add_peer(peer_address);
if (err != ESP_OK) {
@@ -410,7 +381,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl
return ESP_ERR_ESPNOW_NO_MEM;
}
// Load the packet data
packet->load_data(peer_address, payload, size, callback);
packet->load_data(peer_address, payload, size, std::move(callback));
// Push the packet to the send queue
this->send_packet_queue_.push(packet);
// Loop may be disabled while idle; re-enable it to send the packet
@@ -418,6 +389,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl
this->enable_loop_soon_any_context();
return ESP_OK;
}
// NOLINTEND(clang-analyzer-unix.Malloc)
void ESPNowComponent::send_() {
ESPNowSendPacket *packet = this->send_packet_queue_.pop();
@@ -426,7 +398,7 @@ void ESPNowComponent::send_() {
}
this->current_send_packet_ = packet;
esp_err_t err = esp_now_send(packet->address_, packet->data_, packet->size_);
esp_err_t err = platform::send(packet->address_, packet->data_, packet->size_);
if (err != ESP_OK) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(packet->address_, addr_buf);
@@ -451,13 +423,8 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
return ESP_ERR_INVALID_MAC;
}
if (!esp_now_is_peer_exist(peer)) {
esp_now_peer_info_t peer_info = {};
memset(&peer_info, 0, sizeof(esp_now_peer_info_t));
peer_info.ifidx = WIFI_IF_STA;
memcpy(peer_info.peer_addr, peer, ESP_NOW_ETH_ALEN);
esp_err_t err = esp_now_add_peer(&peer_info);
if (!platform::peer_exists(peer)) {
esp_err_t err = platform::add_peer(peer);
if (err != ESP_OK) {
char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(peer, peer_buf);
@@ -486,8 +453,8 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) {
if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) {
return ESP_ERR_ESPNOW_NOT_INIT;
}
if (esp_now_is_peer_exist(peer)) {
esp_err_t err = esp_now_del_peer(peer);
if (platform::peer_exists(peer)) {
esp_err_t err = platform::del_peer(peer);
if (err != ESP_OK) {
char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(peer, peer_buf);
@@ -507,4 +474,4 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) {
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+12 -23
View File
@@ -4,21 +4,17 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "esphome/core/event_pool.h"
#include "esphome/core/lock_free_queue.h"
#include "espnow_packet.h"
#include "espnow_types.h"
#if defined(USE_WIFI) && defined(USE_WIFI_CONNECT_STATE_LISTENERS)
#include "esphome/components/wifi/wifi_component.h"
#endif
#include <esp_idf_version.h>
#include <esp_mac.h>
#include <esp_now.h>
#include <array>
#include <map>
#include <memory>
@@ -27,10 +23,6 @@
namespace esphome::espnow {
// Maximum size of the ESPNow event queue - must be power of 2 for lock-free queue
static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 16;
static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 16;
using peer_address_t = std::array<uint8_t, ESP_NOW_ETH_ALEN>;
enum class ESPNowTriggers : uint8_t {
@@ -152,12 +144,10 @@ class ESPNowComponent final : public Component {
/// @param payload Data payload to send
/// @param callback Callback to call when the send operation is complete
/// @return ESP_OK on success, or an error code on failure
esp_err_t send(const uint8_t *peer_address, const std::vector<uint8_t> &payload,
const send_callback_t &callback = nullptr) {
return this->send(peer_address, payload.data(), payload.size(), callback);
esp_err_t send(const uint8_t *peer_address, const std::vector<uint8_t> &payload, send_callback_t callback = nullptr) {
return this->send(peer_address, payload.data(), payload.size(), std::move(callback));
}
esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
const send_callback_t &callback = nullptr);
esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size, send_callback_t callback = nullptr);
void register_receive_handler(ESPNowReceivedPacketHandler *handler) { this->receive_handlers_.push_back(handler); }
void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) {
@@ -165,14 +155,13 @@ class ESPNowComponent final : public Component {
}
void register_broadcast_handler(ESPNowBroadcastHandler *handler) { this->broadcast_handlers_.push_back(handler); }
protected:
friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size);
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
friend void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status);
#else
friend void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status);
#endif
// Entry points for the radio driver callbacks in espnow_platform_*.cpp; they run outside the main loop and
// only queue the event
void packet_received(const uint8_t *src_addr, const uint8_t *des_addr, const uint8_t *data, int size, int8_t rssi,
uint32_t timestamp);
void send_reported(const uint8_t *mac_addr, esp_now_send_status_t status);
protected:
void enable_();
void send_();
@@ -205,4 +194,4 @@ extern ESPNowComponent *global_esp_now; // NOLINT(cppcoreguidelines-avoid-non-c
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+9 -10
View File
@@ -1,19 +1,18 @@
#pragma once
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include <esp_err.h>
#include <esp_now.h>
#include "espnow_types.h"
namespace esphome::espnow {
static const esp_err_t ESP_ERR_ESPNOW_CMP_BASE = (ESP_ERR_ESPNOW_BASE + 20);
static const esp_err_t ESP_ERR_ESPNOW_FAILED = (ESP_ERR_ESPNOW_CMP_BASE + 1);
static const esp_err_t ESP_ERR_ESPNOW_OWN_ADDRESS = (ESP_ERR_ESPNOW_CMP_BASE + 2);
static const esp_err_t ESP_ERR_ESPNOW_DATA_SIZE = (ESP_ERR_ESPNOW_CMP_BASE + 3);
static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_SET = (ESP_ERR_ESPNOW_CMP_BASE + 4);
static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_PAIRED = (ESP_ERR_ESPNOW_CMP_BASE + 5);
static constexpr esp_err_t ESP_ERR_ESPNOW_CMP_BASE = (ESP_ERR_ESPNOW_BASE + 20);
static constexpr esp_err_t ESP_ERR_ESPNOW_FAILED = (ESP_ERR_ESPNOW_CMP_BASE + 1);
static constexpr esp_err_t ESP_ERR_ESPNOW_OWN_ADDRESS = (ESP_ERR_ESPNOW_CMP_BASE + 2);
static constexpr esp_err_t ESP_ERR_ESPNOW_DATA_SIZE = (ESP_ERR_ESPNOW_CMP_BASE + 3);
static constexpr esp_err_t ESP_ERR_ESPNOW_PEER_NOT_SET = (ESP_ERR_ESPNOW_CMP_BASE + 4);
static constexpr esp_err_t ESP_ERR_ESPNOW_PEER_NOT_PAIRED = (ESP_ERR_ESPNOW_CMP_BASE + 5);
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+19 -51
View File
@@ -1,19 +1,17 @@
#pragma once
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "espnow_err.h"
#include "espnow_types.h"
#include <cstdint>
#include <cstring>
#include <functional>
#include <memory>
#include <utility>
#include <vector>
#include <esp_err.h>
#include <esp_idf_version.h>
#include <esp_now.h>
namespace esphome::espnow {
static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
@@ -36,16 +34,10 @@ static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN,
"espnow max_payload_size beyond 250 bytes requires an ESP-IDF with ESP-NOW v2 support (5.4+)");
#endif
struct WifiPacketRxControl {
int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm
uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or
// light sleep is not enabled
};
struct ESPNowRecvInfo {
uint8_t src_addr[ESP_NOW_ETH_ALEN]; /**< Source address of ESPNOW packet */
uint8_t des_addr[ESP_NOW_ETH_ALEN]; /**< Destination address of ESPNOW packet */
wifi_pkt_rx_ctrl_t *rx_ctrl; /**< Rx control info of ESPNOW packet */
rx_ctrl_t *rx_ctrl; /**< Rx control info of ESPNOW packet */
};
using send_callback_t = std::function<void(esp_err_t)>;
@@ -58,34 +50,28 @@ class ESPNowPacket {
SENT,
};
// Constructor for received data
ESPNowPacket(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
this->init_received_data_(info, data, size);
};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
// Constructor for sent data
ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) {
this->init_sent_data_(info->src_addr, status);
}
#else
// Constructor for sent data
ESPNowPacket(const uint8_t *mac_addr, esp_now_send_status_t status) { this->init_sent_data_(mac_addr, status); }
#endif
// Default constructor for pre-allocation in pool
ESPNowPacket() {}
void release() {}
void load_received_data(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
void load_received_data(const uint8_t *src_addr, const uint8_t *des_addr, const uint8_t *data, uint16_t size,
int8_t rssi, uint32_t timestamp) {
this->type_ = RECEIVED;
this->init_received_data_(info, data, size);
memcpy(this->packet_.receive.info.src_addr, src_addr, ESP_NOW_ETH_ALEN);
memcpy(this->packet_.receive.info.des_addr, des_addr, ESP_NOW_ETH_ALEN);
memcpy(this->packet_.receive.data, data, size);
this->packet_.receive.size = size;
this->packet_.receive.rx_ctrl.rssi = rssi;
this->packet_.receive.rx_ctrl.timestamp = timestamp;
this->packet_.receive.info.rx_ctrl = reinterpret_cast<rx_ctrl_t *>(&this->packet_.receive.rx_ctrl);
}
void load_sent_data(const uint8_t *mac_addr, esp_now_send_status_t status) {
this->type_ = SENT;
this->init_sent_data_(mac_addr, status);
memcpy(this->packet_.sent.address, mac_addr, ESP_NOW_ETH_ALEN);
this->packet_.sent.status = status;
}
// Disable copy to prevent double-delete
@@ -112,24 +98,6 @@ class ESPNowPacket {
esp_now_packet_type_t type() const { return this->type_; }
const ESPNowRecvInfo &get_receive_info() const { return this->packet_.receive.info; }
private:
void init_received_data_(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
memcpy(this->packet_.receive.info.src_addr, info->src_addr, ESP_NOW_ETH_ALEN);
memcpy(this->packet_.receive.info.des_addr, info->des_addr, ESP_NOW_ETH_ALEN);
memcpy(this->packet_.receive.data, data, size);
this->packet_.receive.size = size;
this->packet_.receive.rx_ctrl.rssi = info->rx_ctrl->rssi;
this->packet_.receive.rx_ctrl.timestamp = info->rx_ctrl->timestamp;
this->packet_.receive.info.rx_ctrl = reinterpret_cast<wifi_pkt_rx_ctrl_t *>(&this->packet_.receive.rx_ctrl);
}
void init_sent_data_(const uint8_t *mac_addr, esp_now_send_status_t status) {
memcpy(this->packet_.sent.address, mac_addr, ESP_NOW_ETH_ALEN);
this->packet_.sent.status = status;
}
};
class ESPNowSendPacket {
@@ -151,9 +119,9 @@ class ESPNowSendPacket {
ESPNowSendPacket(const ESPNowSendPacket &) = delete;
ESPNowSendPacket &operator=(const ESPNowSendPacket &) = delete;
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &callback) {
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size, send_callback_t &&callback) {
this->init_data_(peer_address, payload, size);
this->callback_ = callback;
this->callback_ = std::move(callback);
}
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
@@ -180,4 +148,4 @@ class ESPNowSendPacket {
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
@@ -0,0 +1,34 @@
#pragma once
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "espnow_types.h"
#include <cstdint>
// The radio and ESP-NOW driver calls that differ between ESP-IDF and the ESP8266 NONOS SDK. Each platform
// implements the whole set in its own source file, so the component itself carries no platform checks.
namespace esphome::espnow::platform {
// Bring up the station interface when no wifi component owns the radio
void init_radio();
void set_channel(uint8_t channel);
uint8_t get_channel();
void read_mac(uint8_t *mac);
// Start the driver and route its callbacks to the component
esp_err_t init();
esp_err_t deinit();
uint32_t get_version(); // 0 where the SDK has no version call
bool peer_exists(const uint8_t *mac);
esp_err_t add_peer(const uint8_t *mac);
esp_err_t del_peer(const uint8_t *mac);
// Move the registered peers to the radio's current channel; only the NONOS SDK binds peers to a channel
void rebind_peers();
esp_err_t send(const uint8_t *mac, const uint8_t *data, uint16_t size);
} // namespace esphome::espnow::platform
#endif // USE_ESP32 || USE_ESP8266
@@ -0,0 +1,96 @@
#include "espnow_platform.h"
#ifdef USE_ESP32
#include "espnow_component.h"
#include <esp_idf_version.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <cstring>
namespace esphome::espnow::platform {
static void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
global_esp_now->packet_received(info->src_addr, info->des_addr, data, size, info->rx_ctrl->rssi,
info->rx_ctrl->timestamp);
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
static void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status) {
global_esp_now->send_reported(info->des_addr, status);
}
#else
static void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) {
global_esp_now->send_reported(mac_addr, status);
}
#endif
void init_radio() {
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_disconnect());
}
void set_channel(uint8_t channel) {
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
}
uint8_t get_channel() {
uint8_t channel = 0;
wifi_second_chan_t second;
esp_wifi_get_channel(&channel, &second);
return channel;
}
void read_mac(uint8_t *mac) { esp_wifi_get_mac(WIFI_IF_STA, mac); }
esp_err_t init() {
esp_err_t err = esp_now_init();
if (err == ESP_OK) {
err = esp_now_register_recv_cb(on_data_received);
}
if (err == ESP_OK) {
err = esp_now_register_send_cb(on_send_report);
}
return err;
}
esp_err_t deinit() {
esp_now_unregister_recv_cb();
esp_now_unregister_send_cb();
return esp_now_deinit();
}
uint32_t get_version() {
uint32_t version = 0;
esp_now_get_version(&version);
return version;
}
bool peer_exists(const uint8_t *mac) { return esp_now_is_peer_exist(mac); }
esp_err_t add_peer(const uint8_t *mac) {
esp_now_peer_info_t peer_info = {};
peer_info.ifidx = WIFI_IF_STA;
memcpy(peer_info.peer_addr, mac, ESP_NOW_ETH_ALEN);
return esp_now_add_peer(&peer_info);
}
esp_err_t del_peer(const uint8_t *mac) { return esp_now_del_peer(mac); }
// Peers are added on channel 0, which ESP-IDF treats as whatever channel the radio is on
void rebind_peers() {}
esp_err_t send(const uint8_t *mac, const uint8_t *data, uint16_t size) { return esp_now_send(mac, data, size); }
} // namespace esphome::espnow::platform
#endif // USE_ESP32
@@ -0,0 +1,105 @@
#include "espnow_platform.h"
#ifdef USE_ESP8266
#include "espnow_component.h"
namespace esphome::espnow::platform {
// The SDK reports 0 for success and a non-zero value for any failure
static esp_err_t sdk_result(int result) { return result == 0 ? ESP_OK : ESP_FAIL; }
// Stands in for the destination address, which the SDK does not report
static const uint8_t UNKNOWN_ADDR[ESP_NOW_ETH_ALEN] = {0};
static void on_data_received(uint8_t *mac_addr, uint8_t *data, uint8_t size) {
global_esp_now->packet_received(mac_addr, UNKNOWN_ADDR, data, size, 0, 0);
}
static void on_send_report(uint8_t *mac_addr, uint8_t status) {
global_esp_now->send_reported(mac_addr, status == 0 ? ESP_NOW_SEND_SUCCESS : ESP_NOW_SEND_FAIL);
}
void init_radio() {
// The NONOS SDK only delivers broadcast frames to ESP-NOW while the station is associated or the
// soft-AP interface is up. Without Wi-Fi, run a hidden, open soft-AP that accepts no clients so
// broadcasts (for example from a WizMote style remote) are received.
softap_config ap{};
ap.channel = 1;
ap.authmode = AUTH_OPEN;
ap.ssid_hidden = 1;
ap.max_connection = 0;
ap.beacon_interval = 60000;
wifi_set_opmode_current(STATIONAP_MODE);
wifi_softap_set_config_current(&ap);
wifi_softap_dhcps_stop();
wifi_set_sleep_type(NONE_SLEEP_T);
wifi_station_disconnect();
}
void set_channel(uint8_t channel) {
wifi_promiscuous_enable(true);
wifi_set_channel(channel);
wifi_promiscuous_enable(false);
// Keep the soft-AP from init_radio() on the same channel so the SDK never retunes the radio to its own
softap_config ap{};
if (wifi_get_opmode() == STATIONAP_MODE && wifi_softap_get_config(&ap) && ap.channel != channel) {
ap.channel = channel;
wifi_softap_set_config_current(&ap);
}
}
uint8_t get_channel() { return wifi_get_channel(); }
void read_mac(uint8_t *mac) {
// Frames leave from the station interface unless the device runs as a soft-AP only
wifi_get_macaddr(wifi_get_opmode() == SOFTAP_MODE ? SOFTAP_IF : STATION_IF, mac);
}
esp_err_t init() {
int result = esp_now_init();
if (result == 0) {
// CONTROLLER sends from the station interface; the other roles send from the soft-AP MAC whenever
// that interface is up, so peers would no longer recognise the sender.
result = esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
}
if (result == 0) {
result = esp_now_register_recv_cb(on_data_received);
}
if (result == 0) {
result = esp_now_register_send_cb(on_send_report);
}
return sdk_result(result);
}
esp_err_t deinit() {
esp_now_unregister_recv_cb();
esp_now_unregister_send_cb();
return sdk_result(esp_now_deinit());
}
uint32_t get_version() { return 0; }
// The SDK returns 1 when the peer exists, 0 when it does not and a negative value on error
bool peer_exists(const uint8_t *mac) { return esp_now_is_peer_exist(const_cast<uint8_t *>(mac)) > 0; }
esp_err_t add_peer(const uint8_t *mac) {
return sdk_result(esp_now_add_peer(const_cast<uint8_t *>(mac), ESP_NOW_ROLE_COMBO, wifi_get_channel(), nullptr, 0));
}
esp_err_t del_peer(const uint8_t *mac) { return sdk_result(esp_now_del_peer(const_cast<uint8_t *>(mac))); }
void rebind_peers() {
const uint8_t channel = wifi_get_channel();
for (uint8_t *mac = esp_now_fetch_peer(true); mac != nullptr; mac = esp_now_fetch_peer(false)) {
esp_now_set_peer_channel(mac, channel);
}
}
esp_err_t send(const uint8_t *mac, const uint8_t *data, uint16_t size) {
return sdk_result(esp_now_send(const_cast<uint8_t *>(mac), const_cast<uint8_t *>(data), size));
}
} // namespace esphome::espnow::platform
#endif // USE_ESP8266
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include <cstddef>
#include <cstdint>
#if defined(USE_ESP32)
#include <esp_err.h>
#include <esp_now.h>
#elif defined(USE_ESP8266)
extern "C" {
#include <c_types.h>
#include <espnow.h>
#include <user_interface.h>
}
#endif
namespace esphome::espnow {
struct WifiPacketRxControl {
int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm
uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or
// light sleep is not enabled
};
#if defined(USE_ESP32)
// Handlers read rx_ctrl through the ESP-IDF type so existing lambdas keep compiling
using rx_ctrl_t = wifi_pkt_rx_ctrl_t;
static constexpr bool ESPNOW_REPORTS_DESTINATION = true;
// Maximum size of the ESPNow event queues - must be a power of 2 for the lock-free queue
static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 16;
static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 16;
#elif defined(USE_ESP8266)
// The NONOS SDK has no esp_err_t vocabulary. These are the ESP-IDF names and values the shared espnow code
// uses, so it reads the same on both platforms.
using esp_err_t = int32_t;
static constexpr esp_err_t ESP_OK = 0;
static constexpr esp_err_t ESP_FAIL = -1;
static constexpr esp_err_t ESP_ERR_INVALID_MAC = 0x10B;
static constexpr esp_err_t ESP_ERR_ESPNOW_BASE = 0x3064;
static constexpr esp_err_t ESP_ERR_ESPNOW_NOT_INIT = ESP_ERR_ESPNOW_BASE + 1;
static constexpr esp_err_t ESP_ERR_ESPNOW_ARG = ESP_ERR_ESPNOW_BASE + 2;
static constexpr esp_err_t ESP_ERR_ESPNOW_NO_MEM = ESP_ERR_ESPNOW_BASE + 3;
static constexpr esp_err_t ESP_ERR_ESPNOW_NOT_FOUND = ESP_ERR_ESPNOW_BASE + 5;
static constexpr esp_err_t ESP_ERR_ESPNOW_INTERNAL = ESP_ERR_ESPNOW_BASE + 6;
static constexpr esp_err_t ESP_ERR_ESPNOW_IF = ESP_ERR_ESPNOW_BASE + 8;
inline const char *esp_err_to_name(esp_err_t err) { return err == ESP_OK ? "ESP_OK" : "ESP_FAIL"; }
static constexpr uint8_t ESP_NOW_ETH_ALEN = 6;
static constexpr uint8_t ESP_NOW_MAX_DATA_LEN = 250;
// NOLINTNEXTLINE(readability-identifier-naming)
enum esp_now_send_status_t : uint8_t {
ESP_NOW_SEND_SUCCESS = 0,
ESP_NOW_SEND_FAIL = 1,
};
using rx_ctrl_t = WifiPacketRxControl;
// The SDK reports neither the destination address nor the signal strength of a received frame
static constexpr bool ESPNOW_REPORTS_DESTINATION = false;
// Half the ESP32 depth: the heap is far smaller and only one send is ever in flight
static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 8;
static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 8;
#endif
} // namespace esphome::espnow
@@ -1,10 +1,12 @@
#include "espnow_transport.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include <span>
namespace esphome::espnow {
ESPHOME_LOG_TAG(TAG, "espnow.transport");
@@ -64,9 +66,7 @@ bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data
return false;
}
this->packet_buffer_.resize(size);
memcpy(this->packet_buffer_.data(), data, size);
this->process_(this->packet_buffer_);
this->process_(std::span<const uint8_t>(data, size));
return false; // Allow other handlers to run
}
@@ -79,12 +79,10 @@ bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *da
return false;
}
this->packet_buffer_.resize(size);
memcpy(this->packet_buffer_.data(), data, size);
this->process_(this->packet_buffer_);
this->process_(std::span<const uint8_t>(data, size));
return false; // Allow other handlers to run
}
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
@@ -2,7 +2,7 @@
#include "../espnow_component.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "esphome/core/component.h"
#include "esphome/components/packet_transport/packet_transport.h"
@@ -33,9 +33,8 @@ class ESPNowTransport final : public packet_transport::PacketTransport,
bool should_send() override;
peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
std::vector<uint8_t> packet_buffer_;
};
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+3 -1
View File
@@ -98,7 +98,9 @@ async def setup_event_core_(
) -> None:
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
cg.add(var.set_event_types(event_types))
if event_types:
table = cg.shared_progmem_array("event_types", cg.const_char_ptr, event_types)
cg.add(var.set_event_types_static(table, len(event_types)))
setup_device_class(config)
+2 -5
View File
@@ -28,11 +28,8 @@ void Event::trigger(const char *event_type) {
#endif
}
void Event::set_event_types(const FixedVector<const char *> &event_types) {
this->types_.init(event_types.size());
for (const char *type : event_types) {
this->types_.push_back(type);
}
void Event::set_event_types_copy_(const char *const *event_types, size_t count) {
this->types_.assign_copy(event_types, count);
this->last_event_type_ = nullptr; // Reset when types change
}
+20 -6
View File
@@ -11,6 +11,9 @@
namespace esphome::event {
/// A codegen table in flash, or a heap copy of a runtime list (see ConstVector).
using EventTypes = ConstVector<const char *, true>;
#define LOG_EVENT(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), LOG_STR_ARG((obj)->get_log_name())); \
@@ -24,20 +27,29 @@ class Event : public EntityBase {
void trigger(const char *event_type);
void trigger(const std::string &event_type) { this->trigger(event_type.c_str()); }
/// Set the event types supported by this event; called by generated code with string literals.
/// Codegen only: points at a table that outlives the event (see ConstVector::assign_static).
void set_event_types_static(const char *const *event_types, size_t count) {
this->types_.assign_static(event_types, count);
this->last_event_type_ = nullptr;
}
/// Runtime lists are copied; the strings must still outlive the event.
void set_event_types(std::initializer_list<const char *> event_types) {
this->types_ = event_types;
this->last_event_type_ = nullptr; // Reset when types change
this->set_event_types_copy_(event_types.begin(), event_types.size());
}
/// Copy the event types of another event, for components that wrap one.
void set_event_types(const FixedVector<const char *> &event_types);
void set_event_types(const EventTypes &event_types) {
this->set_event_types_copy_(event_types.data(), event_types.size());
}
void set_event_types(const FixedVector<const char *> &event_types) {
this->set_event_types_copy_(event_types.begin(), event_types.size());
}
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_event_types(std::initializer_list<std::string> event_types) = delete;
void set_event_types(const FixedVector<std::string> &event_types) = delete;
/// Return the event types supported by this event.
const FixedVector<const char *> &get_event_types() const { return this->types_; }
const EventTypes &get_event_types() const { return this->types_; }
/// Return the last triggered event type, or empty StringRef if no event triggered yet.
StringRef get_last_event_type() const { return StringRef::from_maybe_nullptr(this->last_event_type_); }
@@ -68,8 +80,10 @@ class Event : public EntityBase {
}
protected:
void set_event_types_copy_(const char *const *event_types, size_t count);
LazyCallbackManager<void(StringRef event_type)> event_callback_;
FixedVector<const char *> types_;
EventTypes types_;
private:
/// Last triggered event type - must point to entry in types_ to ensure valid lifetime.
+22
View File
@@ -24,6 +24,7 @@ CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop"
CONF_INFER_ENDSTOP_FROM_MOVEMENT = "infer_endstop_from_movement"
CONF_DIRECTION_CHANGE_WAIT_TIME = "direction_change_wait_time"
CONF_ACCELERATION_WAIT_TIME = "acceleration_wait_time"
CONF_OVERSHOOT_DURATION = "overshoot_duration"
CONF_OBSTACLE_ROLLBACK = "obstacle_rollback"
endstop_ns = cg.esphome_ns.namespace("feedback")
@@ -82,6 +83,9 @@ CONFIG_FEEDBACK_COVER_BASE_SCHEMA = (
cv.Optional(
CONF_ACCELERATION_WAIT_TIME, "0s"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_OVERSHOOT_DURATION, "0s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
},
)
@@ -89,10 +93,25 @@ CONFIG_FEEDBACK_COVER_BASE_SCHEMA = (
)
def validate_overshoot(config: ConfigType) -> ConfigType:
if not config[CONF_OVERSHOOT_DURATION].total_milliseconds:
return config
if config[CONF_HAS_BUILT_IN_ENDSTOP]:
raise cv.Invalid(
f"{CONF_OVERSHOOT_DURATION} has no effect with {CONF_HAS_BUILT_IN_ENDSTOP}"
)
if CONF_OPEN_ENDSTOP in config and CONF_CLOSE_ENDSTOP in config:
raise cv.Invalid(
f"{CONF_OVERSHOOT_DURATION} has no effect when both endstop sensors are supplied"
)
return config
CONFIG_SCHEMA = cv.All(
CONFIG_FEEDBACK_COVER_BASE_SCHEMA,
cv.has_none_or_all_keys(CONF_OPEN_SENSOR, CONF_CLOSE_SENSOR),
validate_infer_endstop,
validate_overshoot,
)
@@ -160,4 +179,7 @@ async def to_code(config: ConfigType) -> None:
var.set_direction_change_waittime(config[CONF_DIRECTION_CHANGE_WAIT_TIME])
)
cg.add(var.set_acceleration_wait_time(config[CONF_ACCELERATION_WAIT_TIME]))
if (overshoot := config[CONF_OVERSHOOT_DURATION]).total_milliseconds:
cg.add_define("USE_FEEDBACK_COVER_OVERSHOOT")
cg.add(var.set_overshoot_duration(overshoot))
cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK]))
+24 -2
View File
@@ -79,6 +79,11 @@ void FeedbackCover::dump_config() {
if (this->acceleration_wait_time_) {
ESP_LOGCONFIG(TAG, " Acceleration wait time: %.1fs", this->acceleration_wait_time_ / 1e3f);
}
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
if (this->overshoot_duration_) {
ESP_LOGCONFIG(TAG, " Overshoot duration: %.1fs", this->overshoot_duration_ / 1e3f);
}
#endif
#ifdef USE_BINARY_SENSOR
if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) {
ESP_LOGCONFIG(TAG, " Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100);
@@ -234,10 +239,22 @@ void FeedbackCover::loop() {
// (stoping from endstop sensor is handled in callback)
if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) {
if (this->is_at_target_()) {
if (this->has_built_in_endstop_ &&
(this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) {
const bool at_end = this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED;
if (this->has_built_in_endstop_ && at_end) {
// Don't trigger stop, let the cover stop by itself.
this->set_current_operation_(COVER_OPERATION_IDLE, true);
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
} else if (at_end && this->overshoot_duration_) {
// The define is global, so a cover without the option still lands here with a zero duration
// Keep driving past the end position for a while so accumulated timing error is squeezed out
if (!this->start_overshoot_time_) {
this->start_overshoot_time_ = now;
} else if (now - this->start_overshoot_time_ > this->overshoot_duration_ ||
now - this->start_dir_time_ > this->max_duration_) {
ESP_LOGD(TAG, "'%s' - Overshoot duration reached. Stopping cover.", LOG_STR_ARG(this->get_log_name()));
this->start_direction_(COVER_OPERATION_IDLE);
}
#endif
} else {
this->start_direction_(COVER_OPERATION_IDLE);
}
@@ -340,6 +357,11 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
binary_sensor::BinarySensor *obstacle{nullptr};
#endif
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
// Any new command ends a running overshoot
this->start_overshoot_time_ = 0;
#endif
switch (dir) {
case COVER_OPERATION_IDLE:
trig = &this->stop_trigger_;
@@ -33,6 +33,9 @@ class FeedbackCover final : public cover::Cover, public Component {
void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; }
void set_assumed_state(bool value) { this->assumed_state_ = value; }
void set_max_duration(uint32_t max_duration) { this->max_duration_ = max_duration; }
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
void set_overshoot_duration(uint32_t overshoot_duration) { this->overshoot_duration_ = overshoot_duration; }
#endif
void set_obstacle_rollback(float obstacle_rollback) { this->obstacle_rollback_ = obstacle_rollback; }
void set_update_interval(uint32_t interval) { this->update_interval_ = interval; }
void set_infer_endstop(bool infer_endstop) { this->infer_endstop_ = infer_endstop; }
@@ -67,6 +70,9 @@ class FeedbackCover final : public cover::Cover, public Component {
uint32_t open_duration_{0};
uint32_t close_duration_{0};
uint32_t max_duration_{UINT32_MAX};
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
uint32_t overshoot_duration_{0};
#endif
optional<uint32_t> direction_change_waittime_{};
uint32_t acceleration_wait_time_{0};
bool has_built_in_endstop_{false};
@@ -80,6 +86,9 @@ class FeedbackCover final : public cover::Cover, public Component {
uint32_t last_recompute_time_{0};
uint32_t start_dir_time_{0};
uint32_t last_publish_time_{0};
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
uint32_t start_overshoot_time_{0};
#endif
float target_position_{0};
uint32_t update_interval_{1000};
};
+1 -6
View File
@@ -42,12 +42,7 @@ void FS3000Component::update() {
}
// checksum passes if the modulo 256 sum of the five bytes is 0
uint8_t checksum = 0;
for (uint8_t i : data) {
checksum += i;
}
if (checksum != 0) {
if (sum_bytes(data, sizeof(data)) != 0) {
this->status_set_warning();
ESP_LOGW(TAG, "Checksum failure when reading from FS3000");
return;
@@ -224,11 +224,7 @@ void FujitsuGeneralClimate::transmit_(uint8_t const *message, uint8_t length) {
}
uint8_t FujitsuGeneralClimate::checksum_state_(uint8_t const *message) {
uint8_t checksum = 0;
for (uint8_t i = 7; i < FUJITSU_GENERAL_STATE_MESSAGE_LENGTH - 1; ++i) {
checksum += message[i];
}
return 256 - checksum;
return -sum_bytes(message + 7, FUJITSU_GENERAL_STATE_MESSAGE_LENGTH - 8);
}
uint8_t FujitsuGeneralClimate::checksum_util_(uint8_t const *message) { return 255 - message[5]; }
-2
View File
@@ -37,7 +37,6 @@ void GreeClimate::transmit_state() {
remote_state[1] = this->temperature_();
if (model == GREE_YAN) {
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[4] = this->vertical_swing_();
}
@@ -61,7 +60,6 @@ void GreeClimate::transmit_state() {
}
if (model == GREE_YAA || model == GREE_YAC || model == GREE_YAC1FB9) {
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010
+7 -1
View File
@@ -76,6 +76,12 @@ static constexpr uint8_t GREE_PRESET_NONE = 0x00;
static constexpr uint8_t GREE_PRESET_SLEEP = 0x01;
static constexpr uint8_t GREE_PRESET_SLEEP_BIT = 0x80;
// Mode bits stored in remote_state[2] bits 4..7 for YAN/YAA/YAC/YAC1FB9.
static constexpr uint8_t GREE_MODE_BIT_TURBO = 0x10;
static constexpr uint8_t GREE_MODE_BIT_LIGHT = 0x20;
static constexpr uint8_t GREE_MODE_BIT_HEALTH = 0x40;
static constexpr uint8_t GREE_MODE_BIT_XFAN = 0x80;
// Model codes
enum Model : uint8_t { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG };
@@ -103,7 +109,7 @@ class GreeClimate final : public climate_ir::ClimateIR {
uint8_t preset_();
Model model_{};
uint8_t mode_bits_{0}; // Combined mode bits for remote_state[2]
uint8_t mode_bits_{GREE_MODE_BIT_LIGHT}; // The display light is on until a switch says otherwise
};
} // namespace esphome::gree
+19 -6
View File
@@ -17,12 +17,22 @@ CONF_HEALTH = "health"
CONF_XFAN = "xfan"
CONF_GREE_ID = "gree_id"
# Switch configurations: (config_key, display_name, bit_mask, icon)
# Switch configurations: (config_key, display_name, mode_bit, icon)
SWITCH_CONFIGS = (
(CONF_TURBO, "Gree Turbo Switch", 0x10, "mdi:car-turbocharger"),
(CONF_LIGHT, "Gree Light Switch", 0x20, "mdi:led-outline"),
(CONF_HEALTH, "Gree Health Switch", 0x40, "mdi:pine-tree"),
(CONF_XFAN, "Gree X-FAN Switch", 0x80, "mdi:wall-sconce-flat"),
(
CONF_TURBO,
"Gree Turbo Switch",
gree_ns.GREE_MODE_BIT_TURBO,
"mdi:car-turbocharger",
),
(CONF_LIGHT, "Gree Light Switch", gree_ns.GREE_MODE_BIT_LIGHT, "mdi:led-outline"),
(CONF_HEALTH, "Gree Health Switch", gree_ns.GREE_MODE_BIT_HEALTH, "mdi:pine-tree"),
(
CONF_XFAN,
"Gree X-FAN Switch",
gree_ns.GREE_MODE_BIT_XFAN,
"mdi:wall-sconce-flat",
),
)
SUPPORTED_MODELS = {
@@ -39,7 +49,10 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(key): switch.switch_schema(
GreeModeBitSwitch,
icon=icon,
default_restore_mode="RESTORE_DEFAULT_OFF",
# The display light was on before the switches existed, so it stays on by default
default_restore_mode="RESTORE_DEFAULT_ON"
if key == CONF_LIGHT
else "RESTORE_DEFAULT_OFF",
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
)
+6 -1
View File
@@ -186,7 +186,12 @@ def _base_config_schema(class_: MockObjClass) -> cv.Schema:
cv.Optional(
CONF_ANSWER_TIMEOUT,
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation({}),
# data points into the protocol's receive buffer, reused for the next frame
cv.Optional(
CONF_ON_STATUS_MESSAGE
): automation.validate_synchronous_automation(
"on_status_message", "the received frame"
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
+1 -8
View File
@@ -80,14 +80,7 @@ void HM3301Component::update() {
this->status_clear_warning();
}
bool HM3301Component::validate_checksum_(const uint8_t *data) {
uint8_t sum = 0;
for (int i = 0; i < 28; i++) {
sum += data[i];
}
return sum == data[28];
}
bool HM3301Component::validate_checksum_(const uint8_t *data) { return sum_bytes(data, 28) == data[28]; }
uint16_t HM3301Component::get_sensor_value_(const uint8_t *data, uint8_t i) {
return (uint16_t) data[i * 2] << 8 | data[i * 2 + 1];
+2 -1
View File
@@ -2,7 +2,8 @@ import logging
import esphome.codegen as cg
from esphome.components import i2c, sensor
from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE
from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE
from esphome.components.const import CONF_AQI
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
+25 -16
View File
@@ -4,7 +4,7 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32
from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.components.const import CONF_REQUEST_HEADERS, CONF_VERIFY_SSL
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -39,6 +39,7 @@ HttpRequestIDF = http_request_ns.class_("HttpRequestIDF", HttpRequestComponent)
HttpRequestHost = http_request_ns.class_("HttpRequestHost", HttpRequestComponent)
HttpContainer = http_request_ns.class_("HttpContainer")
RequestHeader = http_request_ns.struct("RequestHeader")
HttpRequestSendAction = http_request_ns.class_(
"HttpRequestSendAction", automation.Action
@@ -53,7 +54,6 @@ HttpRequestResponseTrigger = http_request_ns.class_(
CONF_HTTP_REQUEST_ID = "http_request_id"
CONF_USERAGENT = "useragent"
CONF_VERIFY_SSL = "verify_ssl"
CONF_FOLLOW_REDIRECTS = "follow_redirects"
CONF_REDIRECT_LIMIT = "redirect_limit"
CONF_BUFFER_SIZE_RX = "buffer_size_rx"
@@ -227,14 +227,8 @@ async def to_code(config: ConfigType) -> None:
# use_full_certificate_bundle: true
esp32.require_certificate_bundle()
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_INSECURE",
not config.get(CONF_VERIFY_SSL),
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY",
not config.get(CONF_VERIFY_SSL),
)
else:
esp32.allow_insecure_tls()
if CORE.is_esp8266:
cg.add_library("ESP8266HTTPClient", None)
if CORE.is_rp2 and CORE.using_arduino:
@@ -367,12 +361,27 @@ async def http_request_action_to_code(
for key in json_:
template_ = await cg.templatable(json_[key], args, cg.std_string)
cg.add(var.add_json(key, template_))
request_headers = config.get(CONF_REQUEST_HEADERS, {})
if request_headers:
cg.add(var.init_request_headers(len(request_headers)))
for key, value in request_headers.items():
template_ = await cg.templatable(value, args, cg.const_char_ptr)
cg.add(var.add_request_header(key, template_))
if request_headers := config.get(CONF_REQUEST_HEADERS):
# A user lambda may keep static state, so only all-constant lists are shared
share = not any(map(cg.is_template, request_headers.values()))
# Constant headers ignore their arguments, so drop the names: identical lists
# from scripts with different parameter names then render the same table.
header_args = [(arg_type, "") for arg_type, _ in args] if share else args
headers = [
cg.StructInitializer(
"",
("name", key),
("value", await cg.templatable(value, header_args, cg.const_char_ptr)),
)
for key, value in request_headers.items()
]
table = cg.shared_progmem_array(
"http_request_headers",
RequestHeader.template(*[arg_type for arg_type, _ in args]),
headers,
share=share,
)
cg.add(var.set_request_headers(table, len(headers)))
for value in config.get(CONF_COLLECT_HEADERS, []):
cg.add(var.add_collect_header(value.lower()))
+15 -6
View File
@@ -22,6 +22,12 @@ struct Header {
std::string value;
};
/// One request header of an action; codegen puts these in a shared flash table.
template<typename... Ts> struct RequestHeader {
const char *name;
const char *(*value)(Ts...);
};
// Some common HTTP status codes
enum HttpStatus {
HTTP_STATUS_OK = 200,
@@ -492,9 +498,12 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
TEMPLATABLE_VALUE(bool, capture_response)
#endif
void init_request_headers(size_t count) { this->request_headers_.init(count); }
void add_request_header(const char *key, TemplatableFn<const char *, Ts...> value) {
this->request_headers_.push_back({key, value});
static_assert(std::is_trivially_copyable_v<RequestHeader<Ts...>> &&
sizeof(RequestHeader<Ts...>) % sizeof(uint32_t) == 0,
"ESP8266 reads request headers from flash in whole words");
/// Codegen only: the table must outlive the action.
void set_request_headers(const RequestHeader<Ts...> *headers, size_t count) {
this->request_headers_ = ConstVector<RequestHeader<Ts...>>(headers, count);
}
void add_collect_header(const char *value) { this->lower_case_collect_headers_.emplace_back(value); }
@@ -530,8 +539,8 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
}
std::vector<Header> request_headers;
request_headers.reserve(this->request_headers_.size());
for (const auto &[key, val] : this->request_headers_) {
request_headers.push_back({key, val.value(x...)});
for (const auto &header : this->request_headers_) {
request_headers.push_back({header.name, header.value(x...)});
}
auto container = this->parent_->start(this->url_.value(x...).c_str(), this->method_.value(x...), body,
@@ -596,7 +605,7 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
}
}
HttpRequestComponent *parent_;
FixedVector<std::pair<const char *, TemplatableFn<const char *, Ts...>>> request_headers_{};
ConstVector<RequestHeader<Ts...>> request_headers_{};
std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"};
FixedVector<std::pair<const char *, TemplatableValue<std::string, Ts...>>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
+2 -4
View File
@@ -601,10 +601,8 @@ async def to_code(config: ConfigType) -> None:
pins_struct = _build_pins_struct(pin_expressions, e_pin_num)
hub75_config = _build_config_struct(config, pins_struct, min_refresh)
# Rotation is handled by the hub75 driver (config_.rotation already set above).
# Force rotation to 0 for ESPHome's Display base class to avoid double-rotation.
if CONF_ROTATION in config:
config[CONF_ROTATION] = 0
# The driver rotates the panel itself (config_.rotation above), so the base class must not rotate too
config.pop(CONF_ROTATION, None)
# Create display and register
var = cg.new_Pvariable(config[CONF_ID], hub75_config)
+40
View File
@@ -202,6 +202,46 @@ void HUB75Display::set_brightness(uint8_t brightness) {
}
}
static Hub75Rotation to_hub75_rotation(display::DisplayRotation rotation) {
switch (rotation) {
case display::DisplayRotation::DISPLAY_ROTATION_90_DEGREES:
return Hub75Rotation::ROTATE_90;
case display::DisplayRotation::DISPLAY_ROTATION_180_DEGREES:
return Hub75Rotation::ROTATE_180;
case display::DisplayRotation::DISPLAY_ROTATION_270_DEGREES:
return Hub75Rotation::ROTATE_270;
default:
return Hub75Rotation::ROTATE_0;
}
}
static display::DisplayRotation to_display_rotation(Hub75Rotation rotation) {
switch (rotation) {
case Hub75Rotation::ROTATE_90:
return display::DisplayRotation::DISPLAY_ROTATION_90_DEGREES;
case Hub75Rotation::ROTATE_180:
return display::DisplayRotation::DISPLAY_ROTATION_180_DEGREES;
case Hub75Rotation::ROTATE_270:
return display::DisplayRotation::DISPLAY_ROTATION_270_DEGREES;
default:
return display::DisplayRotation::DISPLAY_ROTATION_0_DEGREES;
}
}
// Before setup() creates the driver the configuration holds the rotation, afterwards the driver does
void HUB75Display::set_rotation(display::DisplayRotation rotation) {
const Hub75Rotation hub75_rotation = to_hub75_rotation(rotation);
if (this->driver_ == nullptr) {
this->config_.rotation = hub75_rotation;
return;
}
this->driver_->set_rotation(hub75_rotation);
}
display::DisplayRotation HUB75Display::get_rotation() const {
return to_display_rotation(this->driver_ == nullptr ? this->config_.rotation : this->driver_->get_rotation());
}
} // namespace esphome::hub75
#endif
+4 -1
View File
@@ -36,6 +36,9 @@ class HUB75Display final : public display::Display {
// Brightness control (runtime mutable)
void set_brightness(uint8_t brightness);
void set_rotation(display::DisplayRotation rotation) override;
display::DisplayRotation get_rotation() const override;
protected:
// Display internal methods
int get_width_internal() override { return this->driver_ != nullptr ? this->driver_->get_width() : 0; }
@@ -43,7 +46,7 @@ class HUB75Display final : public display::Display {
// Member variables
Hub75Driver *driver_{nullptr};
Hub75Config config_; // Immutable configuration
Hub75Config config_; // Fixed after setup(); only the rotation can change before the driver exists
// Runtime state (mutable)
uint8_t brightness_{128};
+10
View File
@@ -4,6 +4,7 @@
#include <utility>
#include <vector>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
namespace esphome::i2c {
@@ -53,6 +54,15 @@ class I2CBus {
return this->write_readv(address, buffer, len, nullptr, 0);
}
#ifdef I2C_PORT_FREQUENCY_COUNT
/// @brief Switch the bus frequency for the transactions that follow; an external bus
/// that does not override this reports an error and keeps its frequency
virtual ErrorCode switch_frequency(uint32_t frequency) { return ERROR_UNKNOWN; }
/// @brief The frequency the bus runs at
virtual uint32_t get_frequency() const { return 0; }
#endif
protected:
/// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair
/// that contains the address and the corresponding bool presence flag.
+53 -40
View File
@@ -15,7 +15,7 @@ ESPHOME_LOG_TAG(TAG, "i2c");
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
void ArduinoI2CBus::setup() {
recover_();
this->recover_();
#if defined(USE_ESP8266)
wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory)
@@ -25,9 +25,9 @@ void ArduinoI2CBus::setup() {
// RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
// RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf
if ((this->sda_pin_ / 2) % 2 == 0) {
wire_ = &Wire;
this->wire_ = &Wire;
} else {
wire_ = &Wire1;
this->wire_ = &Wire1;
}
#endif
@@ -42,22 +42,22 @@ void ArduinoI2CBus::setup() {
void ArduinoI2CBus::set_pins_and_clock_() {
#ifdef USE_RP2
wire_->setSDA(this->sda_pin_);
wire_->setSCL(this->scl_pin_);
wire_->begin();
this->wire_->setSDA(this->sda_pin_);
this->wire_->setSCL(this->scl_pin_);
this->wire_->begin();
#else
wire_->begin(static_cast<int>(sda_pin_), static_cast<int>(scl_pin_));
this->wire_->begin(static_cast<int>(this->sda_pin_), static_cast<int>(this->scl_pin_));
#endif
if (timeout_ > 0) { // if timeout specified in yaml
if (this->timeout_ > 0) { // if timeout specified in yaml
#if defined(USE_ESP8266)
// https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h
wire_->setClockStretchLimit(timeout_); // unit: us
this->wire_->setClockStretchLimit(this->timeout_); // unit: us
#elif defined(USE_RP2)
// https://github.com/earlephilhower/ArduinoCore-API/blob/e37df85425e0ac020bfad226d927f9b00d2e0fb7/api/Stream.h
wire_->setTimeout(timeout_ / 1000); // unit: ms
this->wire_->setTimeout(this->timeout_ / 1000); // unit: ms
#endif
}
wire_->setClock(frequency_);
this->wire_->setClock(this->frequency_);
}
void ArduinoI2CBus::dump_config() {
@@ -67,7 +67,7 @@ void ArduinoI2CBus::dump_config() {
" SCL Pin: GPIO%u\n"
" Frequency: %u Hz",
this->sda_pin_, this->scl_pin_, this->frequency_);
if (timeout_ > 0) {
if (this->timeout_ > 0) {
#if defined(USE_ESP8266)
ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_);
#elif defined(USE_RP2)
@@ -87,10 +87,10 @@ void ArduinoI2CBus::dump_config() {
}
if (this->scan_) {
ESP_LOGI(TAG, "Results from bus scan:");
if (scan_results_.empty()) {
if (this->scan_results_.empty()) {
ESP_LOGI(TAG, "Found no devices");
} else {
for (const auto &s : scan_results_) {
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGI(TAG, "Found device at address 0x%02X", s.first);
} else {
@@ -106,7 +106,7 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
#if defined(USE_ESP8266)
this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances
#endif
if (!initialized_) {
if (!this->initialized_) {
ESP_LOGD(TAG, "i2c bus not initialized!");
return ERROR_NOT_INITIALIZED;
}
@@ -118,22 +118,22 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
uint8_t status = 0;
if (write_count != 0 || read_count == 0) {
wire_->beginTransmission(address);
size_t ret = wire_->write(write_buffer, write_count);
this->wire_->beginTransmission(address);
size_t ret = this->wire_->write(write_buffer, write_count);
if (ret != write_count) {
ESP_LOGV(TAG, "TX failed");
return ERROR_UNKNOWN;
}
status = wire_->endTransmission(read_count == 0);
status = this->wire_->endTransmission(read_count == 0);
}
if (status == 0 && read_count != 0) {
size_t ret2 = wire_->requestFrom(address, read_count, true);
size_t ret2 = this->wire_->requestFrom(address, read_count, true);
if (ret2 != read_count) {
ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", read_count, address, ret2);
return ERROR_TIMEOUT;
}
for (size_t j = 0; j != read_count; j++)
read_buffer[j] = wire_->read();
read_buffer[j] = this->wire_->read();
}
// Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266
if (status == 0)
@@ -154,6 +154,19 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
return ERROR_UNKNOWN;
}
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode ArduinoI2CBus::switch_frequency(uint32_t frequency) {
if (this->frequency_ == frequency) {
return ERROR_OK;
}
this->frequency_ = frequency;
if (this->initialized_) {
this->wire_->setClock(this->frequency_);
}
return ERROR_OK;
}
#endif
/// Perform I2C bus recovery, see:
/// https://www.nxp.com/docs/en/user-guide/UM10204.pdf
/// https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf
@@ -167,15 +180,15 @@ void ArduinoI2CBus::recover_() {
const auto half_period_usec = 1000000 / 100000 / 2;
// Activate input and pull up resistor for the SCL pin.
pinMode(scl_pin_, INPUT_PULLUP); // NOLINT
pinMode(this->scl_pin_, INPUT_PULLUP); // NOLINT
// This should make the signal on the line HIGH. If SCL is pulled low
// on the I2C bus however, then some device is interfering with the SCL
// line. In that case, the I2C bus cannot be recovered.
delayMicroseconds(half_period_usec);
if (digitalRead(scl_pin_) == LOW) { // NOLINT
if (digitalRead(this->scl_pin_) == LOW) { // NOLINT
ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the bus");
recovery_result_ = RECOVERY_FAILED_SCL_LOW;
this->recovery_result_ = RECOVERY_FAILED_SCL_LOW;
return;
}
@@ -189,18 +202,18 @@ void ArduinoI2CBus::recover_() {
// Make sure that switching to output mode will make SCL low, just in
// case other code has setup the pin for a HIGH signal.
digitalWrite(scl_pin_, LOW); // NOLINT
digitalWrite(this->scl_pin_, LOW); // NOLINT
delayMicroseconds(half_period_usec);
for (auto i = 0; i < 9; i++) {
// Release pull up resistor and switch to output to make the signal LOW.
pinMode(scl_pin_, INPUT); // NOLINT
pinMode(scl_pin_, OUTPUT); // NOLINT
pinMode(this->scl_pin_, INPUT); // NOLINT
pinMode(this->scl_pin_, OUTPUT); // NOLINT
delayMicroseconds(half_period_usec);
// Release output and activate pull up resistor to make the signal HIGH.
pinMode(scl_pin_, INPUT); // NOLINT
pinMode(scl_pin_, INPUT_PULLUP); // NOLINT
pinMode(this->scl_pin_, INPUT); // NOLINT
pinMode(this->scl_pin_, INPUT_PULLUP); // NOLINT
delayMicroseconds(half_period_usec);
// When SCL is kept LOW at this point, we might be looking at a device
@@ -211,28 +224,28 @@ void ArduinoI2CBus::recover_() {
// should recover in a few ms or less else not likely to recovery at
// all.
auto wait = 250;
while (wait-- && digitalRead(scl_pin_) == LOW) { // NOLINT
while (wait-- && digitalRead(this->scl_pin_) == LOW) { // NOLINT
App.feed_wdt();
delayMicroseconds(half_period_usec * 2);
}
if (digitalRead(scl_pin_) == LOW) { // NOLINT
if (digitalRead(this->scl_pin_) == LOW) { // NOLINT
ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle");
recovery_result_ = RECOVERY_FAILED_SCL_LOW;
this->recovery_result_ = RECOVERY_FAILED_SCL_LOW;
return;
}
}
// Activate input and pull resistor for the SDA pin, so we can verify
// that SDA is pulled HIGH in the following step.
pinMode(sda_pin_, INPUT_PULLUP); // NOLINT
digitalWrite(sda_pin_, LOW); // NOLINT
pinMode(this->sda_pin_, INPUT_PULLUP); // NOLINT
digitalWrite(this->sda_pin_, LOW); // NOLINT
// By now, any stuck device ought to have sent all remaining bits of its
// transaction, meaning that it should have freed up the SDA line, resulting
// in SDA being pulled up.
if (digitalRead(sda_pin_) == LOW) { // NOLINT
if (digitalRead(this->sda_pin_) == LOW) { // NOLINT
ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle");
recovery_result_ = RECOVERY_FAILED_SDA_LOW;
this->recovery_result_ = RECOVERY_FAILED_SDA_LOW;
return;
}
@@ -248,8 +261,8 @@ void ArduinoI2CBus::recover_() {
// SCL and SDA are already high at this point, so we can generate a START
// condition by making the SDA signal LOW.
delayMicroseconds(half_period_usec);
pinMode(sda_pin_, INPUT); // NOLINT
pinMode(sda_pin_, OUTPUT); // NOLINT
pinMode(this->sda_pin_, INPUT); // NOLINT
pinMode(this->sda_pin_, OUTPUT); // NOLINT
// From the specification:
// "A START condition immediately followed by a STOP condition (void
@@ -258,10 +271,10 @@ void ArduinoI2CBus::recover_() {
// Finally, we'll bring the I2C bus into a starting state by generating
// a STOP condition.
delayMicroseconds(half_period_usec);
pinMode(sda_pin_, INPUT); // NOLINT
pinMode(sda_pin_, INPUT_PULLUP); // NOLINT
pinMode(this->sda_pin_, INPUT); // NOLINT
pinMode(this->sda_pin_, INPUT_PULLUP); // NOLINT
recovery_result_ = RECOVERY_COMPLETED;
this->recovery_result_ = RECOVERY_COMPLETED;
}
} // namespace esphome::i2c
+9 -5
View File
@@ -22,13 +22,17 @@ class ArduinoI2CBus final : public InternalI2CBus, public Component {
size_t read_count) override;
float get_setup_priority() const override { return setup_priority::BUS; }
void set_scan(bool scan) { scan_ = scan; }
void set_sda_pin(uint8_t sda_pin) { sda_pin_ = sda_pin; }
void set_scl_pin(uint8_t scl_pin) { scl_pin_ = scl_pin; }
void set_frequency(uint32_t frequency) { frequency_ = frequency; }
void set_timeout(uint32_t timeout) { timeout_ = timeout; }
void set_scan(bool scan) { this->scan_ = scan; }
void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; }
void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
int get_port() const override { return 0; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode switch_frequency(uint32_t frequency) override;
uint32_t get_frequency() const override { return this->frequency_; }
#endif
private:
void recover_();
+72 -24
View File
@@ -32,8 +32,8 @@ void IDFI2CBus::setup() {
i2c_master_bus_config_t bus_conf{};
memset(&bus_conf, 0, sizeof(bus_conf));
bus_conf.sda_io_num = gpio_num_t(sda_pin_);
bus_conf.scl_io_num = gpio_num_t(scl_pin_);
bus_conf.sda_io_num = gpio_num_t(this->sda_pin_);
bus_conf.scl_io_num = gpio_num_t(this->scl_pin_);
bus_conf.glitch_ignore_cnt = 7;
#if SOC_LP_I2C_SUPPORTED
if (this->lp_mode_) {
@@ -59,7 +59,7 @@ void IDFI2CBus::setup() {
}
#endif
bus_conf.i2c_port = this->port_;
bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_;
bus_conf.flags.enable_internal_pullup = this->sda_pullup_enabled_ || this->scl_pullup_enabled_;
esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err));
@@ -67,18 +67,21 @@ void IDFI2CBus::setup() {
return;
}
i2c_device_config_t dev_conf{};
memset(&dev_conf, 0, sizeof(dev_conf));
dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7;
dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED;
dev_conf.scl_speed_hz = this->frequency_;
dev_conf.scl_wait_us = this->timeout_;
err = i2c_master_bus_add_device(this->bus_, &dev_conf, &this->dev_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err));
this->dev_ = this->add_device_(this->frequency_);
if (this->dev_ == nullptr) {
this->mark_failed();
return;
}
#ifdef I2C_PORT_FREQUENCY_COUNT
this->add_frequency(this->frequency_);
for (auto &device : this->devices_) {
device.dev = device.frequency == this->frequency_ ? this->dev_ : this->add_device_(device.frequency);
if (device.dev == nullptr) {
this->mark_failed();
return;
}
}
#endif
this->initialized_ = true;
@@ -88,6 +91,21 @@ void IDFI2CBus::setup() {
}
}
i2c_master_dev_handle_t IDFI2CBus::add_device_(uint32_t frequency) {
i2c_device_config_t dev_conf{};
dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7;
dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED;
dev_conf.scl_speed_hz = frequency;
dev_conf.scl_wait_us = this->timeout_;
i2c_master_dev_handle_t dev{};
const esp_err_t err = i2c_master_bus_add_device(this->bus_, &dev_conf, &dev);
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err));
return nullptr;
}
return dev;
}
void IDFI2CBus::dump_config() {
ESP_LOGCONFIG(TAG, "I2C Bus:");
ESP_LOGCONFIG(TAG,
@@ -95,7 +113,7 @@ void IDFI2CBus::dump_config() {
" SCL Pin: GPIO%u\n"
" Frequency: %" PRIu32 " Hz",
this->sda_pin_, this->scl_pin_, this->frequency_);
if (timeout_ > 0) {
if (this->timeout_ > 0) {
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 "us", this->timeout_);
}
switch (this->recovery_result_) {
@@ -111,10 +129,10 @@ void IDFI2CBus::dump_config() {
}
if (this->scan_) {
ESP_LOGCONFIG(TAG, "Results from bus scan:");
if (scan_results_.empty()) {
if (this->scan_results_.empty()) {
ESP_LOGCONFIG(TAG, "Found no devices");
} else {
for (const auto &s : scan_results_) {
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
@@ -129,7 +147,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
size_t read_count) {
// logging is only enabled with v level, if warnings are shown the caller
// should log them
if (!initialized_) {
if (!this->initialized_) {
ESP_LOGW(TAG, "i2c bus not initialized!");
return ERROR_NOT_INITIALIZED;
}
@@ -198,14 +216,44 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
return ERROR_OK;
}
#ifdef I2C_PORT_FREQUENCY_COUNT
void IDFI2CBus::add_frequency(uint32_t frequency) {
for (const auto &device : this->devices_) {
if (device.frequency == frequency) {
return;
}
}
this->devices_.push_back({frequency, nullptr});
}
ErrorCode IDFI2CBus::switch_frequency(uint32_t frequency) {
if (this->frequency_ == frequency) {
return ERROR_OK;
}
if (!this->initialized_) {
this->frequency_ = frequency;
return ERROR_OK;
}
// Every handle exists since setup(); a frequency nobody registered has none
for (const auto &device : this->devices_) {
if (device.frequency == frequency) {
this->dev_ = device.dev;
this->frequency_ = frequency;
return ERROR_OK;
}
}
return ERROR_UNKNOWN;
}
#endif
/// Perform I2C bus recovery, see:
/// https://www.nxp.com/docs/en/user-guide/UM10204.pdf
/// https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf
void IDFI2CBus::recover_() {
ESP_LOGI(TAG, "Performing bus recovery");
const auto scl_pin = static_cast<gpio_num_t>(scl_pin_);
const auto sda_pin = static_cast<gpio_num_t>(sda_pin_);
const auto scl_pin = static_cast<gpio_num_t>(this->scl_pin_);
const auto sda_pin = static_cast<gpio_num_t>(this->sda_pin_);
// For the upcoming operations, target for a 60kHz toggle frequency.
// 1000kHz is the maximum frequency for I2C running in standard-mode,
@@ -218,7 +266,7 @@ void IDFI2CBus::recover_() {
// Configure SCL pin for open drain input/output, with a pull up resistor.
gpio_set_level(scl_pin, 1);
gpio_config_t scl_config{};
scl_config.pin_bit_mask = 1ULL << scl_pin_;
scl_config.pin_bit_mask = 1ULL << this->scl_pin_;
scl_config.mode = GPIO_MODE_INPUT_OUTPUT_OD;
scl_config.pull_up_en = GPIO_PULLUP_ENABLE;
scl_config.pull_down_en = GPIO_PULLDOWN_DISABLE;
@@ -228,7 +276,7 @@ void IDFI2CBus::recover_() {
// Configure SDA pin for open drain input/output, with a pull up resistor.
gpio_set_level(sda_pin, 1);
gpio_config_t sda_conf{};
sda_conf.pin_bit_mask = 1ULL << sda_pin_;
sda_conf.pin_bit_mask = 1ULL << this->sda_pin_;
sda_conf.mode = GPIO_MODE_INPUT_OUTPUT_OD;
sda_conf.pull_up_en = GPIO_PULLUP_ENABLE;
sda_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
@@ -240,7 +288,7 @@ void IDFI2CBus::recover_() {
delayMicroseconds(half_period_usec);
if (gpio_get_level(scl_pin) == 0) {
ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the bus");
recovery_result_ = RECOVERY_FAILED_SCL_LOW;
this->recovery_result_ = RECOVERY_FAILED_SCL_LOW;
return;
}
@@ -271,7 +319,7 @@ void IDFI2CBus::recover_() {
}
if (gpio_get_level(scl_pin) == 0) {
ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle");
recovery_result_ = RECOVERY_FAILED_SCL_LOW;
this->recovery_result_ = RECOVERY_FAILED_SCL_LOW;
return;
}
}
@@ -281,7 +329,7 @@ void IDFI2CBus::recover_() {
// in SDA being pulled up.
if (gpio_get_level(sda_pin) == 0) {
ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle");
recovery_result_ = RECOVERY_FAILED_SDA_LOW;
this->recovery_result_ = RECOVERY_FAILED_SDA_LOW;
return;
}
@@ -308,7 +356,7 @@ void IDFI2CBus::recover_() {
delayMicroseconds(half_period_usec);
gpio_set_level(sda_pin, 1);
recovery_result_ = RECOVERY_COMPLETED;
this->recovery_result_ = RECOVERY_COMPLETED;
}
} // namespace esphome::i2c
+15
View File
@@ -34,13 +34,28 @@ class IDFI2CBus final : public InternalI2CBus, public Component {
#endif
int get_port() const override { return this->port_; }
#ifdef I2C_PORT_FREQUENCY_COUNT
/// Register a frequency used on the bus so setup() creates its device handle
void add_frequency(uint32_t frequency);
ErrorCode switch_frequency(uint32_t frequency) override;
uint32_t get_frequency() const override { return this->frequency_; }
#endif
private:
void recover_();
i2c_master_dev_handle_t add_device_(uint32_t frequency);
RecoveryCode recovery_result_{};
protected:
i2c_master_dev_handle_t dev_{};
#ifdef I2C_PORT_FREQUENCY_COUNT
// One handle per frequency used on the bus, registered at code generation
struct FrequencyDevice {
uint32_t frequency;
i2c_master_dev_handle_t dev;
};
StaticVector<FrequencyDevice, I2C_PORT_FREQUENCY_COUNT + 1> devices_;
#endif
i2c_master_bus_handle_t bus_{};
i2c_port_t port_{};
uint8_t sda_pin_{};
+5
View File
@@ -21,7 +21,12 @@ class HostI2CBus final : public I2CBus, public Component {
void set_device(const std::string &device) { this->device_ = device; }
void set_scan(bool scan) { this->scan_ = scan; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
#ifdef I2C_PORT_FREQUENCY_COUNT
// The kernel driver owns the clock, so switch_frequency keeps the base default
uint32_t get_frequency() const override { return this->frequency_; }
#endif
const std::string &get_device() const { return this->device_; }
+26 -7
View File
@@ -120,15 +120,34 @@ ErrorCode ZephyrI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer
return ERROR_OK;
}
void ZephyrI2CBus::set_frequency(uint32_t frequency) {
this->dev_config_ &= ~I2C_SPEED_MASK;
if (frequency >= 400000) {
this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_FAST);
} else {
this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_STANDARD);
}
static uint32_t speed_config(uint32_t dev_config, uint32_t frequency) {
const uint32_t speed = frequency >= 400000 ? I2C_SPEED_FAST : I2C_SPEED_STANDARD;
return (dev_config & ~I2C_SPEED_MASK) | I2C_SPEED_SET(speed);
}
void ZephyrI2CBus::set_frequency(uint32_t frequency) {
#ifdef I2C_PORT_FREQUENCY_COUNT
this->frequency_ = frequency;
#endif
this->dev_config_ = speed_config(this->dev_config_, frequency);
}
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode ZephyrI2CBus::switch_frequency(uint32_t frequency) {
if (this->frequency_ == frequency) {
return ERROR_OK;
}
const uint32_t dev_config = speed_config(this->dev_config_, frequency);
// Until ready, setup() applies dev_config_
if (this->is_ready() && i2c_configure(this->i2c_dev_, dev_config) < 0) {
return ERROR_UNKNOWN;
}
this->dev_config_ = dev_config;
this->frequency_ = frequency;
return ERROR_OK;
}
#endif
} // namespace esphome::i2c
#endif
+7
View File
@@ -22,6 +22,10 @@ class ZephyrI2CBus final : public InternalI2CBus, public Component {
void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; }
void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; }
void set_frequency(uint32_t frequency);
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode switch_frequency(uint32_t frequency) override;
uint32_t get_frequency() const override { return this->frequency_; }
#endif
int get_port() const override { return 0; }
@@ -31,6 +35,9 @@ class ZephyrI2CBus final : public InternalI2CBus, public Component {
uint8_t sda_pin_{};
uint8_t scl_pin_{};
uint32_t dev_config_{};
#ifdef I2C_PORT_FREQUENCY_COUNT
uint32_t frequency_{};
#endif
};
} // namespace esphome::i2c

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