mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Merge remote-tracking branch 'origin/dev' into HEAD
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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_(
|
||||
|
||||
@@ -20,6 +20,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 +90,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 +117,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]
|
||||
|
||||
@@ -66,16 +66,18 @@ void Canbus::add_trigger(CanbusTrigger *trigger) {
|
||||
|
||||
void Canbus::loop() {
|
||||
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 +101,12 @@ 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_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::canbus
|
||||
|
||||
@@ -82,6 +82,7 @@ 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; }
|
||||
|
||||
void add_trigger(CanbusTrigger *trigger);
|
||||
/**
|
||||
@@ -100,8 +101,9 @@ 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};
|
||||
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
|
||||
callback_manager_{};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -8,7 +8,11 @@ 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_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
@@ -56,6 +60,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 +76,4 @@ KEY_METADATA = "metadata"
|
||||
|
||||
UNIT_AMPERE_HOUR = "Ah"
|
||||
UNIT_COUNTS = "#"
|
||||
UNIT_MILLIAMPERE_HOUR = "mAh"
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
#include "daikin.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::daikin {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "daikin.climate");
|
||||
|
||||
static constexpr uint32_t BOOST_TIMEOUT_ID = 0;
|
||||
|
||||
static uint8_t sum_bytes(const uint8_t *data, size_t len) {
|
||||
uint8_t sum = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
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 +158,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 +262,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 +306,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 +325,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -294,7 +294,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()); \
|
||||
}
|
||||
|
||||
@@ -710,7 +710,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.
|
||||
|
||||
@@ -44,13 +44,29 @@ def _get_data() -> EmonTxData:
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
def _synchronous_automation(value: ConfigType) -> ConfigType:
|
||||
"""Reject deferring actions: the received line and parsed JSON are only valid while the
|
||||
automation runs, and DelayAction and friends keep the trigger arguments for later."""
|
||||
if automation.has_non_synchronous_actions(value):
|
||||
raise cv.Invalid(
|
||||
"Deferring actions (delay, wait_until, script.wait, ...) are not allowed in on_json and "
|
||||
"on_data: the received 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
|
||||
|
||||
|
||||
def _synchronous_automation_schema() -> cv.All:
|
||||
return cv.All(automation.validate_automation({}), _synchronous_automation)
|
||||
|
||||
|
||||
# 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 +120,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")]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -18,13 +18,15 @@ void EmonTx::setup() { this->buffer_pos_ = 0; }
|
||||
void EmonTx::loop() {
|
||||
// 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 +44,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 the buffer
|
||||
} 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);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
LazyCallbackManager<void(JsonObject, StringRef)> json_callbacks_;
|
||||
LazyCallbackManager<void(StringRef)> data_callbacks_;
|
||||
uint16_t buffer_pos_{0};
|
||||
bool discarding_line_{false}; // Set after an overflow until the end of that line
|
||||
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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.", this->name_.c_str());
|
||||
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};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
@@ -53,7 +53,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 +226,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import CONF_ADC_AVERAGING, CONF_ADC_TIME
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUS_VOLTAGE,
|
||||
@@ -24,8 +25,6 @@ from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
CONF_ADC_AVERAGING = "adc_averaging"
|
||||
CONF_ADC_TIME = "adc_time"
|
||||
|
||||
ina226_ns = cg.esphome_ns.namespace("ina226")
|
||||
INA226Component = ina226_ns.class_(
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.components.const import UNIT_AMPERE_HOUR
|
||||
from esphome.components.const import CONF_ADC_AVERAGING, CONF_ADC_TIME, UNIT_AMPERE_HOUR
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUS_VOLTAGE,
|
||||
@@ -36,9 +36,7 @@ from esphome.types import ConfigType
|
||||
CODEOWNERS = ["@latonita"]
|
||||
DOMAIN = "ina2xx_base"
|
||||
|
||||
CONF_ADC_AVERAGING = "adc_averaging"
|
||||
CONF_ADC_RANGE = "adc_range"
|
||||
CONF_ADC_TIME = "adc_time"
|
||||
CONF_CHARGE = "charge"
|
||||
CONF_CHARGE_COULOMBS = "charge_coulombs"
|
||||
CONF_ENERGY_JOULES = "energy_joules"
|
||||
|
||||
@@ -145,8 +145,8 @@ IT8951Model(
|
||||
reset_pin=12,
|
||||
cs_pin=10,
|
||||
# Board power-enable rails: 1.8V logic supply (GPIO21) and the EPD supply
|
||||
# (GPIO11). Driven high during setup so no separate power_supply is needed.
|
||||
enable_pin=[21, 11],
|
||||
# (GPIO11). Driven high during setup so no separate power_supply is needed, and held low during deep_sleep.
|
||||
enable_pin=[{"number": 21, "hold_state": True}, {"number": 11, "hold_state": True}],
|
||||
vcom=1400,
|
||||
# reTerminal E1003 panel firmware only accepts the 0x0002 VCOM SET
|
||||
# selector; using the default 0x0001 leaves VCOM unchanged and breaks
|
||||
|
||||
@@ -351,6 +351,12 @@ void IT8951Display::on_safe_shutdown() {
|
||||
this->spi_cmd_(TCON_SLEEP);
|
||||
}
|
||||
|
||||
void IT8951Display::on_powerdown() {
|
||||
for (auto *pin : this->enable_pins_) {
|
||||
pin->digital_write(false);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Init op enqueuers -------------------------------------------------------
|
||||
|
||||
void IT8951Display::enqueue_init_reset_() {
|
||||
|
||||
@@ -150,6 +150,7 @@ class IT8951Display : public Display,
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void on_safe_shutdown() override;
|
||||
void on_powerdown() override;
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
// --- Config setters (called from generated code) ---
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ld2412");
|
||||
@@ -73,8 +76,10 @@ enum PeriodicDataValue : uint8_t {
|
||||
};
|
||||
|
||||
enum AckData : uint8_t {
|
||||
FRAME_LENGTH = 4, // little-endian count of the bytes between it and the footer
|
||||
COMMAND = 6,
|
||||
COMMAND_STATUS = 7,
|
||||
ACK_PAYLOAD = 10,
|
||||
};
|
||||
|
||||
// Memory-efficient lookup tables
|
||||
@@ -191,6 +196,9 @@ static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0x
|
||||
static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5};
|
||||
// MAC address the module uses when Bluetooth is disabled
|
||||
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
|
||||
static constexpr uint8_t GATE_SENS_ACK_SIZE = ACK_PAYLOAD + TOTAL_GATES + HEADER_FOOTER_SIZE;
|
||||
// The length field counts the command word, the status word and the gates
|
||||
static constexpr uint16_t GATE_SENS_ACK_LENGTH = GATE_SENS_ACK_SIZE - COMMAND - HEADER_FOOTER_SIZE;
|
||||
|
||||
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
|
||||
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
|
||||
@@ -240,10 +248,10 @@ void LD2412Component::dump_config() {
|
||||
LOG_NUMBER(" ", "MaxDistanceGate", this->max_distance_gate_number_);
|
||||
LOG_NUMBER(" ", "MinDistanceGate", this->min_distance_gate_number_);
|
||||
LOG_NUMBER(" ", "Timeout", this->timeout_number_);
|
||||
for (number::Number *n : this->gate_move_threshold_numbers_) {
|
||||
for (number::Number *n : this->gate_move_thresholds_.numbers) {
|
||||
LOG_NUMBER(" ", "Move Thresholds", n);
|
||||
}
|
||||
for (number::Number *n : this->gate_still_threshold_numbers_) {
|
||||
for (number::Number *n : this->gate_still_thresholds_.numbers) {
|
||||
LOG_NUMBER(" ", "Still Thresholds", n);
|
||||
}
|
||||
#endif
|
||||
@@ -460,6 +468,13 @@ void set_number_value(number::Number *n, float value) {
|
||||
n->publish_state(value);
|
||||
}
|
||||
}
|
||||
|
||||
// NaN marks a value the module never received
|
||||
static inline bool has_value(number::Number *n) { return n != nullptr && n->has_state() && !std::isnan(n->state); }
|
||||
|
||||
// Timeout ids 0 (still) and 1 (motion) for a write waiting on a re-query
|
||||
static constexpr uint32_t GATE_QUERY_TIMEOUT_ID = 0;
|
||||
static constexpr uint32_t GATE_QUERY_TIMEOUT_MS = 2000;
|
||||
#endif
|
||||
|
||||
bool LD2412Component::handle_ack_data_() {
|
||||
@@ -595,19 +610,34 @@ bool LD2412Component::handle_ack_data_() {
|
||||
ESP_LOGV(TAG, "Handled set light control command");
|
||||
break;
|
||||
|
||||
case CMD_QUERY_MOTION_GATE_SENS: {
|
||||
#ifdef USE_NUMBER
|
||||
for (size_t i = 0; i < this->gate_move_threshold_numbers_.size() && (10 + i) < this->buffer_pos_; i++) {
|
||||
set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i]);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_QUERY_MOTION_GATE_SENS:
|
||||
case CMD_QUERY_STATIC_GATE_SENS: {
|
||||
#ifdef USE_NUMBER
|
||||
for (size_t i = 0; i < this->gate_still_threshold_numbers_.size() && (10 + i) < this->buffer_pos_; i++) {
|
||||
set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i]);
|
||||
const uint16_t declared = encode_uint16(this->buffer_data_[FRAME_LENGTH + 1], this->buffer_data_[FRAME_LENGTH]);
|
||||
if (declared != GATE_SENS_ACK_LENGTH || this->buffer_pos_ > GATE_SENS_ACK_SIZE) {
|
||||
// A truncated answer merged with the next frame, or a length this driver does not know
|
||||
ESP_LOGW(TAG, "Dropping gate threshold answer of %u bytes", this->buffer_pos_);
|
||||
return true;
|
||||
}
|
||||
if (this->buffer_pos_ < GATE_SENS_ACK_SIZE) {
|
||||
return false; // the footer pattern appeared inside the payload, keep reading
|
||||
}
|
||||
const bool motion = this->buffer_data_[COMMAND] == CMD_QUERY_MOTION_GATE_SENS;
|
||||
GateThresholds &group = this->gate_thresholds_(motion);
|
||||
// True only while a held-back write waits for this answer
|
||||
const bool pending = this->cancel_timeout(GATE_QUERY_TIMEOUT_ID + motion);
|
||||
for (size_t i = 0; i < TOTAL_GATES; i++) {
|
||||
group.last_read[i] = this->buffer_data_[ACK_PAYLOAD + i];
|
||||
// The pending write's user values must survive the answer
|
||||
if (!pending || !has_value(group.numbers[i])) {
|
||||
set_number_value(group.numbers[i], group.last_read[i]);
|
||||
}
|
||||
}
|
||||
group.read = true;
|
||||
if (pending) {
|
||||
this->set_config_mode_(true);
|
||||
this->send_gate_thresholds_(motion);
|
||||
this->set_config_mode_(false);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@@ -747,6 +777,11 @@ void LD2412Component::set_engineering_mode(bool enable) {
|
||||
void LD2412Component::factory_reset() {
|
||||
this->set_config_mode_(true);
|
||||
this->send_command_(CMD_FACTORY_RESET, nullptr, 0);
|
||||
#ifdef USE_NUMBER
|
||||
// Reset discards the thresholds; the next write must wait for the re-read
|
||||
this->gate_move_thresholds_.read = false;
|
||||
this->gate_still_thresholds_.read = false;
|
||||
#endif
|
||||
this->set_timeout(2000, [this]() { this->restart_and_read_all_info(); });
|
||||
}
|
||||
|
||||
@@ -805,24 +840,48 @@ void LD2412Component::set_basic_config() {
|
||||
}
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
void LD2412Component::set_gate_threshold() {
|
||||
if (this->gate_move_threshold_numbers_.empty() && this->gate_still_threshold_numbers_.empty()) {
|
||||
return; // No gate threshold numbers set; nothing to do here
|
||||
void LD2412Component::send_gate_thresholds_(bool motion) {
|
||||
GateThresholds &group = this->gate_thresholds_(motion);
|
||||
const uint8_t command = motion ? CMD_MOTION_GATE_SENS : CMD_STATIC_GATE_SENS;
|
||||
uint8_t value[TOTAL_GATES];
|
||||
uint8_t configured = 0;
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
number::Number *n = group.numbers[i];
|
||||
if (has_value(n)) {
|
||||
value[i] = static_cast<uint8_t>(n->state);
|
||||
configured++;
|
||||
} else {
|
||||
value[i] = group.last_read[i];
|
||||
}
|
||||
}
|
||||
uint8_t value[TOTAL_GATES] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
if (configured < TOTAL_GATES && !group.read) {
|
||||
// Unread gates would go out as zero, which is maximum sensitivity
|
||||
ESP_LOGW(TAG, "Command %02X held back until the module reports its gate thresholds", command);
|
||||
this->send_command_(motion ? CMD_QUERY_MOTION_GATE_SENS : CMD_QUERY_STATIC_GATE_SENS, nullptr, 0);
|
||||
this->set_timeout(GATE_QUERY_TIMEOUT_ID + motion, GATE_QUERY_TIMEOUT_MS,
|
||||
[this, motion]() { this->gate_query_timeout_(motion); });
|
||||
return;
|
||||
}
|
||||
this->send_command_(command, value, sizeof(value));
|
||||
}
|
||||
|
||||
void LD2412Component::gate_query_timeout_(bool motion) {
|
||||
ESP_LOGE(TAG, "Command %02X not sent, the module did not report its gate thresholds",
|
||||
motion ? CMD_MOTION_GATE_SENS : CMD_STATIC_GATE_SENS);
|
||||
// The values on hand never reached the module or are stale, so forgetting them loses nothing
|
||||
for (number::Number *n : this->gate_thresholds_(motion).numbers) {
|
||||
if (n != nullptr) {
|
||||
n->publish_state(NAN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LD2412Component::set_gate_threshold(number::Number *n) {
|
||||
// Found by pointer: 14 compares per write beat a flag per number
|
||||
const auto &motion_numbers = this->gate_move_thresholds_.numbers;
|
||||
const bool motion = std::find(motion_numbers.begin(), motion_numbers.end(), n) != motion_numbers.end();
|
||||
this->set_config_mode_(true);
|
||||
if (!this->gate_move_threshold_numbers_.empty()) {
|
||||
for (size_t i = 0; i < this->gate_move_threshold_numbers_.size(); i++) {
|
||||
value[i] = lowbyte(static_cast<int>(this->gate_move_threshold_numbers_[i]->state));
|
||||
}
|
||||
this->send_command_(CMD_MOTION_GATE_SENS, value, sizeof(value));
|
||||
}
|
||||
if (!this->gate_still_threshold_numbers_.empty()) {
|
||||
for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) {
|
||||
value[i] = lowbyte(static_cast<int>(this->gate_still_threshold_numbers_[i]->state));
|
||||
}
|
||||
this->send_command_(CMD_STATIC_GATE_SENS, value, sizeof(value));
|
||||
}
|
||||
this->send_gate_thresholds_(motion);
|
||||
this->set_config_mode_(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -85,11 +85,13 @@ class LD2412Component final : public Component, public uart::UARTDevice {
|
||||
void set_light_out_control();
|
||||
void set_basic_config();
|
||||
#ifdef USE_NUMBER
|
||||
void set_gate_move_threshold_number(uint8_t gate, number::Number *n) { this->gate_move_threshold_numbers_[gate] = n; }
|
||||
void set_gate_still_threshold_number(uint8_t gate, number::Number *n) {
|
||||
this->gate_still_threshold_numbers_[gate] = n;
|
||||
void set_gate_move_threshold_number(uint8_t gate, number::Number *n) {
|
||||
this->gate_move_thresholds_.numbers[gate] = n;
|
||||
}
|
||||
void set_gate_threshold();
|
||||
void set_gate_still_threshold_number(uint8_t gate, number::Number *n) {
|
||||
this->gate_still_thresholds_.numbers[gate] = n;
|
||||
}
|
||||
void set_gate_threshold(number::Number *n);
|
||||
void get_gate_threshold();
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
@@ -118,6 +120,19 @@ class LD2412Component final : public Component, public uart::UARTDevice {
|
||||
void query_light_control_();
|
||||
void restart_();
|
||||
void query_dynamic_background_correction_();
|
||||
#ifdef USE_NUMBER
|
||||
// One command carries all 14 gates, so unconfigured gates get the module's last value
|
||||
struct GateThresholds {
|
||||
std::array<number::Number *, TOTAL_GATES> numbers{};
|
||||
std::array<uint8_t, TOTAL_GATES> last_read{};
|
||||
bool read{false}; // answered since boot or the last factory reset
|
||||
};
|
||||
GateThresholds &gate_thresholds_(bool motion) {
|
||||
return motion ? this->gate_move_thresholds_ : this->gate_still_thresholds_;
|
||||
}
|
||||
void send_gate_thresholds_(bool motion);
|
||||
void gate_query_timeout_(bool motion);
|
||||
#endif
|
||||
|
||||
uint8_t light_function_ = 0;
|
||||
uint8_t light_threshold_ = 0;
|
||||
@@ -129,8 +144,8 @@ class LD2412Component final : public Component, public uart::UARTDevice {
|
||||
bool bluetooth_on_{false};
|
||||
bool dynamic_background_correction_active_{false};
|
||||
#ifdef USE_NUMBER
|
||||
std::array<number::Number *, TOTAL_GATES> gate_move_threshold_numbers_{};
|
||||
std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{};
|
||||
GateThresholds gate_move_thresholds_;
|
||||
GateThresholds gate_still_thresholds_;
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{};
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace esphome::ld2412 {
|
||||
|
||||
void GateThresholdNumber::control(float value) {
|
||||
this->publish_state(value);
|
||||
this->parent_->set_gate_threshold();
|
||||
this->parent_->set_gate_threshold(this);
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
|
||||
@@ -19,10 +19,7 @@ class ESPColorSettable {
|
||||
virtual void lighten(uint8_t delta) = 0;
|
||||
virtual void darken(uint8_t delta) = 0;
|
||||
void set(const ESPHSVColor &color) { this->set_hsv(color); }
|
||||
void set_hsv(const ESPHSVColor &color) {
|
||||
Color rgb = color.to_rgb();
|
||||
this->set_rgb(rgb.r, rgb.g, rgb.b);
|
||||
}
|
||||
void set_hsv(const ESPHSVColor &color) { this->set(color.to_rgb()); }
|
||||
void set_rgb(uint8_t red, uint8_t green, uint8_t blue) {
|
||||
this->set_red(red);
|
||||
this->set_green(green);
|
||||
|
||||
@@ -2,6 +2,7 @@ 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_CO2,
|
||||
@@ -19,7 +20,6 @@ from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
|
||||
CONF_DETECTION_RANGE = "detection_range"
|
||||
|
||||
mhz19_ns = cg.esphome_ns.namespace("mhz19")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from esphome.components.mipi import MODE_RGB
|
||||
from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER
|
||||
|
||||
from .ili import GC9A01A, ILI9341, ILI9342, ST7789V
|
||||
@@ -21,8 +22,10 @@ ST7789V.extend(
|
||||
# fmt: off
|
||||
|
||||
ILI9342.extend(
|
||||
# ESP32-2432S028 CYD board with USB C + Micro USB, has ILI9342 controller
|
||||
# ESP32-2432S028 CYD board with USB C + Micro USB, has ILI9342 controller.
|
||||
# The panel on this board is wired RGB, not the BGR default.
|
||||
"ESP32-2432S028-9342",
|
||||
color_order=MODE_RGB,
|
||||
data_rate="40MHz",
|
||||
cs_pin={"number": 15, "ignore_strapping_warning": True},
|
||||
dc_pin={"number": 2, "ignore_strapping_warning": True},
|
||||
|
||||
@@ -20,6 +20,7 @@ SETFANMODE = {
|
||||
|
||||
CONF_SUPPORTS_DRY = "supports_dry"
|
||||
CONF_SUPPORTS_FAN_ONLY = "supports_fan_only"
|
||||
CONF_FAHRENHEIT_COMPATIBILITY = "fahrenheit_compatibility"
|
||||
|
||||
CONF_HORIZONTAL_DEFAULT = "horizontal_default"
|
||||
HorizontalDirections = mitsubishi_ns.enum("HorizontalDirections")
|
||||
@@ -43,18 +44,22 @@ VERTICAL_DIRECTIONS = {
|
||||
"down": VerticalDirections.VERTICAL_DIRECTION_DOWN,
|
||||
}
|
||||
|
||||
CONF_VERTICAL_VANES = "vertical_vanes"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MitsubishiClimate).extend(
|
||||
{
|
||||
cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE),
|
||||
cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean,
|
||||
cv.Optional(CONF_SUPPORTS_FAN_ONLY, default=False): cv.boolean,
|
||||
cv.Optional(CONF_FAHRENHEIT_COMPATIBILITY, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HORIZONTAL_DEFAULT, default="middle"): cv.enum(
|
||||
HORIZONTAL_DIRECTIONS
|
||||
),
|
||||
cv.Optional(CONF_VERTICAL_DEFAULT, default="middle"): cv.enum(
|
||||
VERTICAL_DIRECTIONS
|
||||
),
|
||||
cv.Optional(CONF_VERTICAL_VANES, default=1): cv.int_range(1, 2),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -65,5 +70,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_fan_mode(config[CONF_SET_FAN_MODE]))
|
||||
cg.add(var.set_supports_dry(config[CONF_SUPPORTS_DRY]))
|
||||
cg.add(var.set_supports_fan_only(config[CONF_SUPPORTS_FAN_ONLY]))
|
||||
cg.add(var.set_fahrenheit_compatibility(config[CONF_FAHRENHEIT_COMPATIBILITY]))
|
||||
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
|
||||
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
|
||||
cg.add(var.set_vertical_vanes(config[CONF_VERTICAL_VANES]))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "mitsubishi.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -26,6 +27,7 @@ const uint8_t MITSUBISHI_WIDE_VANE_SWING = 0xC0;
|
||||
|
||||
const uint8_t MITSUBISHI_FAN_AUTO = 0x00;
|
||||
|
||||
const uint8_t MITSUBISHI_VERTICAL_VANE_MASK = 0x38; // Bits 3,4,5
|
||||
const uint8_t MITSUBISHI_VERTICAL_VANE_SWING = 0x38;
|
||||
|
||||
// const uint8_t MITSUBISHI_AUTO = 0x80;
|
||||
@@ -51,7 +53,33 @@ const uint8_t MITSUBISHI_BYTE02 = 0x26;
|
||||
const uint8_t MITSUBISHI_BYTE03 = 0x01;
|
||||
const uint8_t MITSUBISHI_BYTE04 = 0x00;
|
||||
const uint8_t MITSUBISHI_BYTE13 = 0x00;
|
||||
const uint8_t MITSUBISHI_BYTE16 = 0x00;
|
||||
|
||||
// Byte 7 codes for 61°F through 88°F. Bit 4 is the half degree bit Mitsubishi uses so every Fahrenheit degree
|
||||
// maps to its own code.
|
||||
static const uint8_t FAHRENHEIT_CODES[] PROGMEM = {0x00, 0x10, 0x01, 0x11, 0x02, 0x12, 0x03, 0x04, 0x05, 0x15,
|
||||
0x06, 0x16, 0x07, 0x17, 0x08, 0x18, 0x09, 0x19, 0x0a, 0x1a,
|
||||
0x0b, 0x1b, 0x0c, 0x1c, 0x0d, 0x1d, 0x0e, 0x0f};
|
||||
static constexpr uint8_t FAHRENHEIT_MIN = 61;
|
||||
static constexpr uint8_t MITSUBISHI_HALF_DEGREE = 0x10;
|
||||
|
||||
static uint8_t fahrenheit_code(float celsius) {
|
||||
int fahrenheit = (int) roundf(celsius_to_fahrenheit(celsius));
|
||||
size_t index = clamp<int>(fahrenheit - FAHRENHEIT_MIN, 0, sizeof(FAHRENHEIT_CODES) - 1);
|
||||
return progmem_read_byte(&FAHRENHEIT_CODES[index]);
|
||||
}
|
||||
|
||||
// In Fahrenheit mode a code from the table decodes to the exact Fahrenheit value. Any other code, and every code in
|
||||
// Celsius mode, is a whole degree plus the half degree bit, which a remote set to Fahrenheit also sets.
|
||||
static float decode_temperature(uint8_t code, bool fahrenheit) {
|
||||
if (fahrenheit) {
|
||||
for (size_t i = 0; i < sizeof(FAHRENHEIT_CODES); i++) {
|
||||
if (code == progmem_read_byte(&FAHRENHEIT_CODES[i])) {
|
||||
return fahrenheit_to_celsius(FAHRENHEIT_MIN + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return MITSUBISHI_TEMP_MIN + (code & 0x0F) + ((code & MITSUBISHI_HALF_DEGREE) ? 0.5f : 0.0f);
|
||||
}
|
||||
|
||||
void MitsubishiClimate::transmit_state() {
|
||||
// Byte 0-4: Constant: 0x23, 0xCB, 0x26, 0x01, 0x00
|
||||
@@ -72,7 +100,7 @@ void MitsubishiClimate::transmit_state() {
|
||||
// Byte 13: Constant 0x00
|
||||
// Byte 14: HVAC specfic, i.e. ECONO COOL, CLEAN MODE, always 0x00
|
||||
// Byte 15: HVAC specfic, i.e. POWERFUL, SMART SET, PLASMA, always 0x00
|
||||
// Byte 16: Constant 0x00
|
||||
// Byte 16: Left vane control (for specific models). Constants match right vane controls.
|
||||
// Byte 17: Checksum: SUM[Byte0...Byte16]
|
||||
uint8_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
@@ -113,6 +141,8 @@ void MitsubishiClimate::transmit_state() {
|
||||
// Temperature
|
||||
if (this->mode == climate::CLIMATE_MODE_DRY) {
|
||||
remote_state[7] = 24 - MITSUBISHI_TEMP_MIN; // Remote sends always 24°C if "Dry" mode is selected
|
||||
} else if (this->fahrenheit_compatibility_) {
|
||||
remote_state[7] = fahrenheit_code(this->target_temperature);
|
||||
} else {
|
||||
remote_state[7] = (uint8_t) roundf(
|
||||
clamp<float>(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) - MITSUBISHI_TEMP_MIN);
|
||||
@@ -184,7 +214,7 @@ void MitsubishiClimate::transmit_state() {
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "default_vertical_direction_: %02X", this->default_vertical_direction_);
|
||||
ESP_LOGD(TAG, "Vertical default: 0x%02X, vanes: %u", this->default_vertical_direction_, this->vertical_vanes_);
|
||||
|
||||
// Special modes
|
||||
switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) {
|
||||
@@ -206,6 +236,11 @@ void MitsubishiClimate::transmit_state() {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->vertical_vanes_ > 1) {
|
||||
// Heads with two vertical vanes carry the left vane in byte 16; it follows the right one, presets included
|
||||
remote_state[16] = remote_state[9] & MITSUBISHI_VERTICAL_VANE_MASK;
|
||||
}
|
||||
|
||||
// Checksum
|
||||
for (int i = 0; i < 17; i++) {
|
||||
remote_state[17] += remote_state[i];
|
||||
@@ -266,11 +301,11 @@ bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
}
|
||||
state_frame[pos] = byte;
|
||||
|
||||
// Check Header && Footer
|
||||
// Check Header && Footer. Byte 16 only ever carries the left vane position (bits 3,4,5).
|
||||
if ((pos == 0 && byte != MITSUBISHI_BYTE00) || (pos == 1 && byte != MITSUBISHI_BYTE01) ||
|
||||
(pos == 2 && byte != MITSUBISHI_BYTE02) || (pos == 3 && byte != MITSUBISHI_BYTE03) ||
|
||||
(pos == 4 && byte != MITSUBISHI_BYTE04) || (pos == 13 && byte != MITSUBISHI_BYTE13) ||
|
||||
(pos == 16 && byte != MITSUBISHI_BYTE16)) {
|
||||
(pos == 16 && (byte & ~MITSUBISHI_VERTICAL_VANE_MASK) != 0)) {
|
||||
ESP_LOGV(TAG, "Bytes 0,1,2,3,4,13 or 16 fail - invalid value");
|
||||
return false;
|
||||
}
|
||||
@@ -300,7 +335,7 @@ bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
}
|
||||
|
||||
// Temp
|
||||
this->target_temperature = state_frame[7] + MITSUBISHI_TEMP_MIN;
|
||||
this->target_temperature = decode_temperature(state_frame[7], this->fahrenheit_compatibility_);
|
||||
|
||||
// Fan
|
||||
uint8_t fan = state_frame[9] & 0x07; //(Bit 0,1,2 = Speed)
|
||||
@@ -332,7 +367,9 @@ bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
}
|
||||
|
||||
// Vertical Vane
|
||||
uint8_t vertical_vane = state_frame[9] & 0x38; // Bits 3,4,5
|
||||
// On dual vane heads, the left vane is in [16] and right vane is in [9]. Left is ignored here because
|
||||
// the swing_mode enum doesn't convey that level of detail.
|
||||
uint8_t vertical_vane = state_frame[9] & MITSUBISHI_VERTICAL_VANE_MASK;
|
||||
switch (vertical_vane) {
|
||||
case MITSUBISHI_VERTICAL_VANE_SWING:
|
||||
if (this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -9,6 +10,8 @@ namespace esphome::mitsubishi {
|
||||
// Temperature
|
||||
const uint8_t MITSUBISHI_TEMP_MIN = 16; // Celsius
|
||||
const uint8_t MITSUBISHI_TEMP_MAX = 31; // Celsius
|
||||
// Remotes set to Fahrenheit go up to 88°F, one step past the Celsius range
|
||||
static constexpr float MITSUBISHI_TEMP_MAX_FAHRENHEIT = fahrenheit_to_celsius(88.0f);
|
||||
|
||||
// Fan mode
|
||||
enum SetFanMode : uint8_t {
|
||||
@@ -55,6 +58,12 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
|
||||
void set_supports_fan_only(bool supports_fan_only) {
|
||||
this->set_mode_supported_(climate::CLIMATE_MODE_FAN_ONLY, supports_fan_only);
|
||||
}
|
||||
void set_fahrenheit_compatibility(bool fahrenheit_compatibility) {
|
||||
this->fahrenheit_compatibility_ = fahrenheit_compatibility;
|
||||
if (fahrenheit_compatibility) {
|
||||
this->maximum_temperature_ = MITSUBISHI_TEMP_MAX_FAHRENHEIT;
|
||||
}
|
||||
}
|
||||
|
||||
void set_fan_mode(SetFanMode fan_mode) {
|
||||
this->fan_mode_ = fan_mode;
|
||||
@@ -71,6 +80,7 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
|
||||
void set_vertical_default(VerticalDirection vertical_direction) {
|
||||
this->default_vertical_direction_ = vertical_direction;
|
||||
}
|
||||
void set_vertical_vanes(uint8_t vertical_vanes) { this->vertical_vanes_ = vertical_vanes; }
|
||||
|
||||
protected:
|
||||
// Transmit via IR the state of this climate controller.
|
||||
@@ -80,9 +90,11 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
|
||||
bool parse_state_frame_(const uint8_t frame[]);
|
||||
|
||||
SetFanMode fan_mode_;
|
||||
bool fahrenheit_compatibility_{false};
|
||||
|
||||
HorizontalDirection default_horizontal_direction_;
|
||||
VerticalDirection default_vertical_direction_;
|
||||
uint8_t vertical_vanes_{1};
|
||||
};
|
||||
|
||||
} // namespace esphome::mitsubishi
|
||||
|
||||
@@ -950,7 +950,7 @@ bool ModbusDeviceCommand::sent() {
|
||||
// on_sent() is not a terminal, so nothing is consumed.
|
||||
if (this->device == nullptr)
|
||||
return false;
|
||||
this->device->on_sent(this->frame.pdu());
|
||||
this->device->on_sent(this->frame.address(), this->frame.pdu());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -958,7 +958,7 @@ bool ModbusDeviceCommand::notify_retired() {
|
||||
if (!this->decrement_pending())
|
||||
return false; // nothing owed - stop the sweep draining this entry
|
||||
if (this->device != nullptr)
|
||||
this->device->on_not_sent(this->frame.pdu());
|
||||
this->device->on_not_sent(this->frame.address(), this->frame.pdu());
|
||||
return true; // consumed one debt (delivered, or silent when device-less) - keep draining to zero
|
||||
}
|
||||
|
||||
@@ -969,7 +969,7 @@ bool ModbusDeviceCommand::response(std::span<const uint8_t> response_pdu) {
|
||||
this->decrement_pending();
|
||||
if (this->device == nullptr)
|
||||
return false;
|
||||
this->device->on_response(this->frame.pdu(), response_pdu);
|
||||
this->device->on_response(this->frame.address(), this->frame.pdu(), response_pdu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -979,7 +979,7 @@ bool ModbusDeviceCommand::error(ExceptionCode exception_code) {
|
||||
this->decrement_pending();
|
||||
if (this->device == nullptr)
|
||||
return false;
|
||||
this->device->on_error(this->frame.pdu(), exception_code);
|
||||
this->device->on_error(this->frame.address(), this->frame.pdu(), exception_code);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1004,7 +1004,7 @@ bool ModbusDeviceCommand::timed_out() {
|
||||
return false; // resolved, no one to tell
|
||||
// A cleared frame that timed out still honors a retry: the clear is address-scoped (any device may
|
||||
// call it) while the retry is the owning device's call via on_no_response - the bus obeys the owner.
|
||||
if (this->device->on_no_response(this->frame.pdu()))
|
||||
if (this->device->on_no_response(this->frame.address(), this->frame.pdu()))
|
||||
this->increment_pending(); // granted retry = re-request (capped)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -437,6 +437,21 @@ class ModbusClientDevice {
|
||||
return this->on_modbus_no_response();
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
/// Overloads of the hooks above with the device address of the request first, taken from the request frame, so it
|
||||
/// stays correct after set_address(). The hub calls these; the defaults forward to the hooks above.
|
||||
/// Override one form per hook: overriding an address form drops its forward, so the other form is no longer called.
|
||||
virtual void on_response(uint8_t address, std::span<const uint8_t> request_pdu,
|
||||
std::span<const uint8_t> response_pdu) {
|
||||
this->on_response(request_pdu, response_pdu);
|
||||
}
|
||||
virtual void on_error(uint8_t address, std::span<const uint8_t> request_pdu, ExceptionCode exception_code) {
|
||||
this->on_error(request_pdu, exception_code);
|
||||
}
|
||||
virtual void on_not_sent(uint8_t address, std::span<const uint8_t> request_pdu) { this->on_not_sent(request_pdu); }
|
||||
virtual void on_sent(uint8_t address, std::span<const uint8_t> request_pdu) { this->on_sent(request_pdu); }
|
||||
virtual bool on_no_response(uint8_t address, std::span<const uint8_t> request_pdu) {
|
||||
return this->on_no_response(request_pdu);
|
||||
}
|
||||
// Remove before 2027.2.0
|
||||
ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0")
|
||||
virtual void on_modbus_not_sent() {}
|
||||
|
||||
@@ -261,10 +261,7 @@ async def to_code(config):
|
||||
|
||||
if CORE.is_esp32:
|
||||
esp32.request_http_client()
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True
|
||||
)
|
||||
esp32.allow_insecure_tls()
|
||||
elif CORE.is_esp8266:
|
||||
cg.add_library("ESP8266HTTPClient", None)
|
||||
|
||||
|
||||
@@ -732,13 +732,13 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
|
||||
return True # Handled: PYOCD upload
|
||||
|
||||
# Deferred imports: bleak/smpclient are heavy, only load for BLE/mcumgr paths
|
||||
from .ble_logger import is_mac_address
|
||||
from .ble_logger import is_ble_address
|
||||
from .ota import smpmgr_scan, smpmgr_upload
|
||||
|
||||
if host == "BLE":
|
||||
mcumgr_device = asyncio.run(smpmgr_scan(CORE.name))
|
||||
|
||||
if is_mac_address(host):
|
||||
if is_ble_address(host):
|
||||
mcumgr_device = host
|
||||
|
||||
if mcumgr_device:
|
||||
@@ -753,7 +753,7 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
|
||||
|
||||
def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
|
||||
address = devices[0]
|
||||
from .ble_logger import is_mac_address, logger_connect, logger_scan
|
||||
from .ble_logger import is_ble_address, logger_connect, logger_scan
|
||||
|
||||
if devices[0] == "BLE":
|
||||
ble_device = asyncio.run(logger_scan(CORE.name))
|
||||
@@ -762,7 +762,7 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
|
||||
else:
|
||||
return True
|
||||
|
||||
if is_mac_address(address):
|
||||
if is_ble_address(address):
|
||||
asyncio.run(logger_connect(address))
|
||||
return True
|
||||
return False
|
||||
@@ -835,7 +835,7 @@ _PCH_CMAKE_LINES = [
|
||||
"set(esphome_kept_options)",
|
||||
"set(esphome_pch_headers)",
|
||||
"foreach(option IN LISTS esphome_options)",
|
||||
' if(option MATCHES "imacros> (.+)$")',
|
||||
' if(option MATCHES "imacros> ([^>]+)")',
|
||||
' list(APPEND esphome_pch_headers "${CMAKE_MATCH_1}")',
|
||||
" list(APPEND esphome_kept_options",
|
||||
' "$<$<NOT:$<AND:$<COMPILE_LANGUAGE:CXX>,$<STREQUAL:$<TARGET_PROPERTY:NAME>,app>>>:${option}>")',
|
||||
|
||||
@@ -4,6 +4,7 @@ import re
|
||||
from typing import Final
|
||||
|
||||
from bleak import BleakClient, BleakScanner, BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
from bleak.exc import (
|
||||
BleakCharacteristicNotFoundError,
|
||||
BleakDBusError,
|
||||
@@ -19,15 +20,31 @@ NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
MAC_ADDRESS_PATTERN: Final = re.compile(
|
||||
r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE
|
||||
)
|
||||
# macOS identifies BLE peripherals by a CoreBluetooth UUID instead of a MAC address
|
||||
UUID_PATTERN: Final = re.compile(
|
||||
r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$",
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def is_mac_address(value: str) -> bool:
|
||||
return MAC_ADDRESS_PATTERN.match(value)
|
||||
def is_ble_address(value: str) -> bool:
|
||||
return bool(MAC_ADDRESS_PATTERN.match(value) or UUID_PATTERN.match(value))
|
||||
|
||||
|
||||
def _name_matches(device: BLEDevice, adv: AdvertisementData, name: str) -> bool:
|
||||
# device.name can be a stale cached name on macOS; adv.local_name is what is on the air now
|
||||
return name in (device.name, adv.local_name)
|
||||
|
||||
|
||||
async def find_device_by_name(name: str, timeout: float = 10.0) -> BLEDevice | None:
|
||||
return await BleakScanner.find_device_by_filter(
|
||||
lambda device, adv: _name_matches(device, adv, name), timeout=timeout
|
||||
)
|
||||
|
||||
|
||||
async def logger_scan(name: str) -> BLEDevice | None:
|
||||
_LOGGER.info("Scanning bluetooth for %s...", name)
|
||||
device = await BleakScanner.find_device_by_name(name)
|
||||
device = await find_device_by_name(name)
|
||||
if not device:
|
||||
_LOGGER.error("%s Bluetooth LE device was not found!", name)
|
||||
return device
|
||||
|
||||
@@ -4,7 +4,6 @@ import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from bleak import BleakScanner
|
||||
from bleak.exc import BleakDBusError, BleakDeviceNotFoundError
|
||||
from smp.exceptions import SMPBadStartDelimiter
|
||||
from smpclient import SMPClient
|
||||
@@ -23,9 +22,8 @@ from smpclient.transport.serial import SMPSerialTransport
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.espota2 import ProgressBar
|
||||
|
||||
from .ble_logger import is_mac_address
|
||||
from .ble_logger import find_device_by_name, is_ble_address
|
||||
|
||||
SMP_SERVICE_UUID = "8D53DC1D-1DB7-4CD3-868B-8A527460AA84"
|
||||
BLE_SCAN_TIMEOUT = 10.0 # seconds
|
||||
RESET_DELAY = 2.0 # seconds to wait before reset, allows on_end action to execute
|
||||
|
||||
@@ -45,12 +43,12 @@ def _json_state(o: object) -> object:
|
||||
|
||||
async def smpmgr_scan(name: str) -> str:
|
||||
_LOGGER.info("Scanning bluetooth for %s...", name)
|
||||
for device in await BleakScanner.discover(
|
||||
timeout=BLE_SCAN_TIMEOUT, service_uuids=[SMP_SERVICE_UUID]
|
||||
):
|
||||
if device.name == name:
|
||||
return device.address
|
||||
raise EsphomeError(f"BLE device {name} with OTA service not found")
|
||||
# No service filter: the SMP UUID only fits in the scan response, which macOS does not filter on.
|
||||
# The SMP service is checked on connect.
|
||||
device = await find_device_by_name(name, timeout=BLE_SCAN_TIMEOUT)
|
||||
if device is None:
|
||||
raise EsphomeError(f"BLE device {name} not found")
|
||||
return device.address
|
||||
|
||||
|
||||
async def smpmgr_upload(device: str, firmware: Path) -> None:
|
||||
@@ -88,7 +86,7 @@ def _get_image_tlv_sha256(file: Path) -> bytes:
|
||||
async def _smpmgr_upload(device: str, firmware: Path) -> None:
|
||||
image_tlv_sha256 = _get_image_tlv_sha256(firmware)
|
||||
|
||||
if is_mac_address(device):
|
||||
if is_ble_address(device):
|
||||
smp_client = SMPClient(SMPBLETransport(), device)
|
||||
else:
|
||||
smp_client = SMPClient(SMPSerialTransport(), device)
|
||||
@@ -105,7 +103,8 @@ async def _smpmgr_upload(device: str, firmware: Path) -> None:
|
||||
) from exc
|
||||
raise EsphomeError(f"BLE error connecting to {device}: {exc}") from exc
|
||||
except SMPBLETransportException as exc:
|
||||
raise EsphomeError(f"Connection error with {device}") from exc
|
||||
# Reached when a device with the right name has no SMP service, among other causes
|
||||
raise EsphomeError(f"Connection error with {device}: {exc}") from exc
|
||||
|
||||
_LOGGER.info("Connected %s...", device)
|
||||
try:
|
||||
|
||||
@@ -68,8 +68,9 @@ def raise_first_undefined(
|
||||
for e, p_path, _ in errors[1:]
|
||||
)
|
||||
_LOGGER.debug("Additional undefined variables in %s: %s", context_label, extras)
|
||||
raise cv.Invalid(
|
||||
f"Undefined variable in {context_label}: {err.message}\n{format_path(err_path, err_value)}"
|
||||
raise cv.SourceTraceInvalid(
|
||||
f"Undefined variable in {context_label}: {err.message}",
|
||||
format_path(err_path, err_value),
|
||||
)
|
||||
|
||||
|
||||
@@ -223,13 +224,13 @@ def _expand_substitutions(
|
||||
errors=errors,
|
||||
)
|
||||
except JinjaError as err:
|
||||
raise cv.Invalid(
|
||||
raise cv.SourceTraceInvalid(
|
||||
f"{err.error_name()} Error evaluating jinja expression"
|
||||
f" '{value}': {str(err.parent())}."
|
||||
f"\nEvaluation stack: (most recent evaluation last)"
|
||||
f"\n{err.stack_trace_str()}"
|
||||
f"\nRelevant context:\n{err.context_trace_str()}"
|
||||
f"\n{format_path(path, orig_value)}",
|
||||
f"\nRelevant context:\n{err.context_trace_str()}",
|
||||
format_path(path, orig_value),
|
||||
path,
|
||||
) from err
|
||||
else:
|
||||
@@ -389,9 +390,9 @@ def resolve_include(
|
||||
return include.load() if include.should_load() else {}
|
||||
except (esphome.core.EsphomeError, cv.Invalid) as err:
|
||||
resolved = f" (expanded from '{original}')" if substituted else ""
|
||||
raise cv.Invalid(
|
||||
f"Error including file '{filename}'{resolved}: {err}"
|
||||
f"\n{format_path(path, original)}",
|
||||
raise cv.SourceTraceInvalid(
|
||||
f"Error including file '{filename}'{resolved}: {err}",
|
||||
format_path(path, original),
|
||||
path + [f"<{filename}>"],
|
||||
) from err
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ SystaBus = systa_bus_ns.class_("SystaBus", cg.Component, uart.UARTDevice)
|
||||
|
||||
CONF_SYSTA_BUS_ID = "systa_bus_id"
|
||||
|
||||
CONF_SYSTASOLAR_AQUA = "systasolar_aqua"
|
||||
|
||||
CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SystaBus),
|
||||
|
||||
@@ -13,11 +13,16 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_SYSTA_BUS_ID, SystaBus, register_systa_bus_listener, systa_bus_ns
|
||||
from .. import (
|
||||
CONF_SYSTA_BUS_ID,
|
||||
CONF_SYSTASOLAR_AQUA,
|
||||
SystaBus,
|
||||
register_systa_bus_listener,
|
||||
systa_bus_ns,
|
||||
)
|
||||
|
||||
SystaSolarAquaSensor = systa_bus_ns.class_("SystaSolarAquaSensor", cg.Component)
|
||||
|
||||
CONF_SYSTASOLAR_AQUA = "systasolar_aqua"
|
||||
CONF_TEMPERATURE_TSA = "temperature_tsa"
|
||||
CONF_TEMPERATURE_TSE = "temperature_tse"
|
||||
CONF_TEMPERATURE_TWU = "temperature_twu"
|
||||
|
||||
@@ -14,7 +14,8 @@ static constexpr uint8_t START_BYTE = 0xfc;
|
||||
static constexpr uint8_t FRAME_OVERHEAD = 3; // start byte, length, checksum
|
||||
|
||||
// SystaSolar Aqua sensor data, 25 bytes: [0..1] type, [2..3] not decoded, big-endian int16 in 0.1 degrees at
|
||||
// [4..5] TSA, [6..7] TSE, [8..9] TWU and [10..11] TW2, pump speed in percent at [12], [13..23] not decoded.
|
||||
// [4..5] TSA, [6..7] TSE, [8..9] TWU and [10..11] TW2, pump speed in percent at [12], [13..14] not decoded,
|
||||
// [15] error code (zero for no error), [16..23] not decoded.
|
||||
static constexpr uint16_t MESSAGE_TYPE_AQUA_SENSOR_DATA = 0xfc16;
|
||||
|
||||
// The bus only buffers known message types, so the buffer is sized by the longest of them
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MODEL
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_SYSTA_BUS_ID,
|
||||
CONF_SYSTASOLAR_AQUA,
|
||||
SystaBus,
|
||||
register_systa_bus_listener,
|
||||
systa_bus_ns,
|
||||
)
|
||||
|
||||
SystaSolarAquaTextSensor = systa_bus_ns.class_("SystaSolarAquaTextSensor", cg.Component)
|
||||
|
||||
CONF_ERROR_CODE = "error_code"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
CONF_SYSTASOLAR_AQUA: cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SystaSolarAquaTextSensor),
|
||||
cv.GenerateID(CONF_SYSTA_BUS_ID): cv.use_id(SystaBus),
|
||||
cv.Optional(CONF_ERROR_CODE): text_sensor.text_sensor_schema(),
|
||||
}
|
||||
),
|
||||
},
|
||||
key=CONF_MODEL,
|
||||
lower=True,
|
||||
space="_",
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if config[CONF_MODEL] == CONF_SYSTASOLAR_AQUA and CONF_ERROR_CODE in config:
|
||||
sens = await text_sensor.new_text_sensor(config[CONF_ERROR_CODE])
|
||||
cg.add(var.set_error_code_text_sensor(sens))
|
||||
|
||||
systa_bus = await cg.get_variable(config[CONF_SYSTA_BUS_ID])
|
||||
await register_systa_bus_listener(systa_bus, var)
|
||||
@@ -0,0 +1,27 @@
|
||||
#include "systa_bus_text_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::systa_bus {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "systa_bus.text_sensor");
|
||||
|
||||
void SystaSolarAquaTextSensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SystaSolar Aqua:");
|
||||
LOG_TEXT_SENSOR(" ", "Error code", this->error_code_text_sensor_);
|
||||
}
|
||||
|
||||
void SystaSolarAquaTextSensor::handle_message(std::span<const uint8_t> message) {
|
||||
if (get_message_type(message) == MESSAGE_TYPE_AQUA_SENSOR_DATA) {
|
||||
if (this->error_code_text_sensor_ != nullptr) {
|
||||
if (message[15] == 0) {
|
||||
this->error_code_text_sensor_->publish_state("--");
|
||||
} else {
|
||||
char buf[4];
|
||||
int len = snprintf(buf, sizeof(buf), "%02d", message[15]);
|
||||
this->error_code_text_sensor_->publish_state(buf, static_cast<size_t>(len));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::systa_bus
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "../systa_bus.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
#include <span>
|
||||
|
||||
namespace esphome::systa_bus {
|
||||
|
||||
class SystaSolarAquaTextSensor : public SystaBusListener, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void set_error_code_text_sensor(text_sensor::TextSensor *sensor) { this->error_code_text_sensor_ = sensor; }
|
||||
void handle_message(std::span<const uint8_t> message) override;
|
||||
|
||||
protected:
|
||||
text_sensor::TextSensor *error_code_text_sensor_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::systa_bus
|
||||
@@ -1,7 +1,14 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID
|
||||
from esphome.const import (
|
||||
CONF_CHANNEL,
|
||||
CONF_CHANNELS,
|
||||
CONF_FREQUENCY,
|
||||
CONF_I2C_ID,
|
||||
CONF_ID,
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@andreashergert1984"]
|
||||
@@ -16,14 +23,51 @@ TCA9548AChannel = tca9548a_ns.class_("TCA9548AChannel", i2c.I2CBus)
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_BUS_ID = "bus_id"
|
||||
# Sizes the root bus's frequency table; only emitted when a frequency is set
|
||||
_request_port_frequency_slot = cg.slot_counter("I2C_PORT_FREQUENCY_COUNT")
|
||||
|
||||
|
||||
def _root_bus_id(bus_id: ID) -> ID:
|
||||
"""The hardware bus behind a port, through any multiplexers in between."""
|
||||
parents = {
|
||||
str(chan[CONF_BUS_ID]): mux[CONF_I2C_ID]
|
||||
for mux in CORE.config.get(DOMAIN, [])
|
||||
for chan in mux.get(CONF_CHANNELS, [])
|
||||
}
|
||||
while (parent := parents.get(str(bus_id))) is not None:
|
||||
bus_id = parent
|
||||
return bus_id
|
||||
|
||||
|
||||
def _declared_bus_type(bus_id: ID) -> cg.MockObjClass | None:
|
||||
"""The class the i2c component declared for a bus, or None for an external bus."""
|
||||
for bus in CORE.config.get(i2c.DOMAIN, []):
|
||||
if bus[CONF_ID].id == bus_id.id:
|
||||
return bus[CONF_ID].type
|
||||
return None
|
||||
|
||||
|
||||
async def _add_frequency(root_bus_id: ID, frequency: int) -> None:
|
||||
"""Count the frequency against the root bus; the built-in ESP32 bus also
|
||||
creates its device handle in setup()."""
|
||||
_request_port_frequency_slot(str(root_bus_id))
|
||||
if _declared_bus_type(root_bus_id) is i2c.IDFI2CBus:
|
||||
root = await cg.get_variable(root_bus_id)
|
||||
cg.add(root.add_frequency(frequency))
|
||||
|
||||
|
||||
_FREQUENCY = cv.All(cv.frequency, cv.Range(min=0, min_included=False))
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(TCA9548AComponent),
|
||||
cv.Optional(CONF_FREQUENCY): _FREQUENCY,
|
||||
cv.Optional(CONF_CHANNELS, default=[]): cv.ensure_list(
|
||||
{
|
||||
cv.Required(CONF_BUS_ID): cv.declare_id(TCA9548AChannel),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
|
||||
cv.Optional(CONF_FREQUENCY): _FREQUENCY,
|
||||
}
|
||||
),
|
||||
}
|
||||
@@ -37,8 +81,15 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
root_bus_id = _root_bus_id(config[CONF_I2C_ID])
|
||||
if (frequency := config.get(CONF_FREQUENCY)) is not None:
|
||||
await _add_frequency(root_bus_id, int(frequency))
|
||||
cg.add(var.set_frequency(int(frequency)))
|
||||
|
||||
for conf in config[CONF_CHANNELS]:
|
||||
chan = cg.new_Pvariable(conf[CONF_BUS_ID])
|
||||
cg.add(chan.set_parent(var))
|
||||
cg.add(chan.set_channel(conf[CONF_CHANNEL]))
|
||||
if (frequency := conf.get(CONF_FREQUENCY)) is not None:
|
||||
await _add_frequency(root_bus_id, int(frequency))
|
||||
cg.add(chan.set_frequency(int(frequency)))
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "tca9548a.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::tca9548a {
|
||||
@@ -7,20 +9,111 @@ ESPHOME_LOG_TAG(TAG, "tca9548a");
|
||||
|
||||
i2c::ErrorCode TCA9548AChannel::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
|
||||
uint8_t *read_buffer, size_t read_count) {
|
||||
auto err = this->parent_->switch_to_channel(channel_);
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
const uint32_t select_frequency = this->select_frequency_();
|
||||
if (select_frequency != 0) {
|
||||
return this->write_readv_at_port_frequency_(select_frequency, address, write_buffer, write_count, read_buffer,
|
||||
read_count);
|
||||
}
|
||||
#endif
|
||||
auto err = this->parent_->switch_to_channel(this->channel_);
|
||||
if (err != i2c::ERROR_OK)
|
||||
return err;
|
||||
err = this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count);
|
||||
this->parent_->disable_all_channels();
|
||||
return err;
|
||||
}
|
||||
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
// The multiplexer's own frequency, else the slower of the bus and the port
|
||||
uint32_t TCA9548AChannel::select_frequency_() const {
|
||||
if (this->parent_->frequency_ != 0)
|
||||
return this->parent_->frequency_;
|
||||
if (this->frequency_ == 0)
|
||||
return 0;
|
||||
// A bus that does not report its frequency gets the port's, and fails the switch
|
||||
const uint32_t bus_frequency = this->parent_->bus_->get_frequency();
|
||||
return bus_frequency != 0 ? std::min(this->frequency_, bus_frequency) : this->frequency_;
|
||||
}
|
||||
|
||||
// The port frequency, else the multiplexer's
|
||||
uint32_t TCA9548AChannel::transfer_frequency_() const {
|
||||
return this->frequency_ != 0 ? this->frequency_ : this->parent_->frequency_;
|
||||
}
|
||||
|
||||
i2c::ErrorCode TCA9548AChannel::write_readv_at_port_frequency_(uint32_t select_frequency, uint8_t address,
|
||||
const uint8_t *write_buffer, size_t write_count,
|
||||
uint8_t *read_buffer, size_t read_count) {
|
||||
const uint32_t original_frequency = this->parent_->bus_->get_frequency();
|
||||
// A failed switch leaves the bus as it was
|
||||
auto err = this->switch_bus_(select_frequency);
|
||||
if (err != i2c::ERROR_OK)
|
||||
return err;
|
||||
|
||||
err = this->parent_->switch_to_channel(this->channel_);
|
||||
if (err == i2c::ERROR_OK) {
|
||||
err = this->transfer_(select_frequency, address, write_buffer, write_count, read_buffer, read_count);
|
||||
this->parent_->disable_all_channels();
|
||||
}
|
||||
const i2c::ErrorCode restored = this->switch_bus_(original_frequency);
|
||||
return err != i2c::ERROR_OK ? err : restored;
|
||||
}
|
||||
|
||||
// Transfer at the port frequency, back to the select frequency for the deselect
|
||||
i2c::ErrorCode TCA9548AChannel::transfer_(uint32_t select_frequency, uint8_t address, const uint8_t *write_buffer,
|
||||
size_t write_count, uint8_t *read_buffer, size_t read_count) {
|
||||
const uint32_t transfer_frequency = this->transfer_frequency_();
|
||||
if (transfer_frequency == select_frequency) {
|
||||
return this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count);
|
||||
}
|
||||
auto err = this->switch_bus_(transfer_frequency);
|
||||
if (err == i2c::ERROR_OK) {
|
||||
err = this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count);
|
||||
}
|
||||
const i2c::ErrorCode restored = this->switch_bus_(select_frequency);
|
||||
return err != i2c::ERROR_OK ? err : restored;
|
||||
}
|
||||
|
||||
// Forward to the shared upstream bus
|
||||
i2c::ErrorCode TCA9548AChannel::switch_frequency(uint32_t frequency) {
|
||||
return this->parent_->bus_->switch_frequency(frequency);
|
||||
}
|
||||
|
||||
i2c::ErrorCode TCA9548AChannel::switch_bus_(uint32_t frequency) {
|
||||
const i2c::ErrorCode err = this->switch_frequency(frequency);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->parent_->status_set_error(LOG_STR("Failed to switch the bus frequency"));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
uint32_t TCA9548AChannel::get_frequency() const { return this->parent_->bus_->get_frequency(); }
|
||||
#endif
|
||||
|
||||
void TCA9548AComponent::setup() {
|
||||
uint8_t status = 0;
|
||||
if (this->read(&status, 1) != i2c::ERROR_OK) {
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
const uint32_t original_frequency = this->bus_->get_frequency();
|
||||
if (this->frequency_ != 0 && this->bus_->switch_frequency(this->frequency_) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR("Failed to switch the bus frequency"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
const i2c::ErrorCode err = this->read(&status, 1);
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
const bool restored = this->frequency_ == 0 || this->bus_->switch_frequency(original_frequency) == i2c::ERROR_OK;
|
||||
#endif
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "TCA9548A failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
if (!restored) {
|
||||
this->mark_failed(LOG_STR("Failed to switch the bus frequency"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
ESP_LOGD(TAG, "Channels currently open: %d", status);
|
||||
}
|
||||
void TCA9548AComponent::dump_config() {
|
||||
@@ -32,14 +125,13 @@ i2c::ErrorCode TCA9548AComponent::switch_to_channel(uint8_t channel) {
|
||||
if (this->is_failed())
|
||||
return i2c::ERROR_NOT_INITIALIZED;
|
||||
|
||||
uint8_t channel_val = 1 << channel;
|
||||
const uint8_t channel_val = 1 << channel;
|
||||
return this->write(&channel_val, 1);
|
||||
}
|
||||
|
||||
void TCA9548AComponent::disable_all_channels() {
|
||||
if (this->write(&TCA9548A_DISABLE_CHANNELS_COMMAND, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to disable all channels.");
|
||||
this->status_set_error(); // couldn't disable channels, set error status
|
||||
this->status_set_error(LOG_STR("Failed to disable all channels."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,30 @@ static const uint8_t TCA9548A_DISABLE_CHANNELS_COMMAND = 0x00;
|
||||
class TCA9548AComponent;
|
||||
class TCA9548AChannel final : public i2c::I2CBus {
|
||||
public:
|
||||
void set_channel(uint8_t channel) { channel_ = channel; }
|
||||
void set_parent(TCA9548AComponent *parent) { parent_ = parent; }
|
||||
void set_channel(uint8_t channel) { this->channel_ = channel; }
|
||||
void set_parent(TCA9548AComponent *parent) { this->parent_ = parent; }
|
||||
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
|
||||
size_t read_count) override;
|
||||
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
|
||||
i2c::ErrorCode switch_frequency(uint32_t frequency) override;
|
||||
uint32_t get_frequency() const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
uint32_t select_frequency_() const;
|
||||
uint32_t transfer_frequency_() const;
|
||||
i2c::ErrorCode write_readv_at_port_frequency_(uint32_t select_frequency, uint8_t address, const uint8_t *write_buffer,
|
||||
size_t write_count, uint8_t *read_buffer, size_t read_count);
|
||||
i2c::ErrorCode transfer_(uint32_t select_frequency, uint8_t address, const uint8_t *write_buffer, size_t write_count,
|
||||
uint8_t *read_buffer, size_t read_count);
|
||||
/// Switch the upstream bus, flagging the multiplexer on failure
|
||||
i2c::ErrorCode switch_bus_(uint32_t frequency);
|
||||
uint32_t frequency_{0};
|
||||
#endif
|
||||
uint8_t channel_;
|
||||
TCA9548AComponent *parent_;
|
||||
};
|
||||
@@ -30,8 +47,15 @@ class TCA9548AComponent final : public Component, public i2c::I2CDevice {
|
||||
|
||||
i2c::ErrorCode switch_to_channel(uint8_t channel);
|
||||
void disable_all_channels();
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
/// Addressing frequency, and the default for the ports
|
||||
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
friend class TCA9548AChannel;
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
uint32_t frequency_{0};
|
||||
#endif
|
||||
};
|
||||
} // namespace esphome::tca9548a
|
||||
|
||||
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@0hax"]
|
||||
CODEOWNERS = ["@0hax", "@FredM67"]
|
||||
DOMAIN = "teleinfo"
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ICON_FLASH,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_KILOVOLT_AMPS,
|
||||
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_KILOWATT,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_MILLIAMP,
|
||||
UNIT_MILLIVOLT,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
@@ -13,19 +35,125 @@ from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, telein
|
||||
|
||||
TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(
|
||||
TeleInfoSensor,
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
icon=ICON_FLASH,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
).extend(TELEINFO_LISTENER_SCHEMA)
|
||||
|
||||
def _preset(unit: str, device_class: str, state_class: str) -> dict[str, str]:
|
||||
return {
|
||||
CONF_UNIT_OF_MEASUREMENT: unit,
|
||||
CONF_DEVICE_CLASS: device_class,
|
||||
CONF_STATE_CLASS: state_class,
|
||||
}
|
||||
|
||||
|
||||
_ENERGY = _preset(UNIT_WATT_HOURS, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING)
|
||||
_REACTIVE_ENERGY = _preset(
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
)
|
||||
_CURRENT = _preset(UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT)
|
||||
_VOLTAGE = _preset(UNIT_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT)
|
||||
_APPARENT_POWER_VA = _preset(
|
||||
UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
|
||||
)
|
||||
_APPARENT_POWER_KVA = _preset(
|
||||
UNIT_KILOVOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
|
||||
)
|
||||
_POWER = _preset(UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT)
|
||||
|
||||
# Legacy defaults, kept for tags without a known preset
|
||||
_LEGACY_DEFAULT = {**_ENERGY, CONF_ICON: ICON_FLASH}
|
||||
|
||||
# Units that are valid for each device class, used to decide whether a preset's
|
||||
# device class can be kept when the user overrides the unit (and vice versa)
|
||||
_COMPATIBLE_UNITS = {
|
||||
DEVICE_CLASS_ENERGY: {UNIT_WATT_HOURS, UNIT_KILOWATT_HOURS},
|
||||
DEVICE_CLASS_REACTIVE_ENERGY: {
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
},
|
||||
DEVICE_CLASS_CURRENT: {UNIT_AMPERE, UNIT_MILLIAMP},
|
||||
DEVICE_CLASS_VOLTAGE: {UNIT_VOLT, UNIT_MILLIVOLT},
|
||||
DEVICE_CLASS_APPARENT_POWER: {UNIT_VOLT_AMPS, UNIT_KILOVOLT_AMPS},
|
||||
DEVICE_CLASS_POWER: {UNIT_WATT, UNIT_KILOWATT},
|
||||
}
|
||||
|
||||
# Presets by tag prefix. No prefix is a prefix of another, so order is irrelevant.
|
||||
TIC_TAG_CONFIGS = {
|
||||
# Standard mode
|
||||
"EA": _ENERGY,
|
||||
"ER": _REACTIVE_ENERGY,
|
||||
"IRMS": _CURRENT,
|
||||
"URMS": _VOLTAGE,
|
||||
"UMOY": _VOLTAGE,
|
||||
"SINST": _APPARENT_POWER_VA,
|
||||
"SMAX": _APPARENT_POWER_VA,
|
||||
"CC": _POWER,
|
||||
"PREF": _APPARENT_POWER_KVA,
|
||||
"PCOUP": _APPARENT_POWER_KVA,
|
||||
# Historical mode
|
||||
"BASE": _ENERGY,
|
||||
"HCH": _ENERGY,
|
||||
"EJP": _ENERGY,
|
||||
"BBRH": _ENERGY,
|
||||
"IINST": _CURRENT,
|
||||
"IMAX": _CURRENT,
|
||||
"ISOUSC": _CURRENT,
|
||||
"ADPS": _CURRENT,
|
||||
"ADIR": _CURRENT,
|
||||
"PAPP": _APPARENT_POWER_VA,
|
||||
"PMAX": _POWER,
|
||||
}
|
||||
|
||||
|
||||
def apply_tag_config(config: ConfigType) -> ConfigType:
|
||||
"""Apply preset configurations based on the tag name.
|
||||
|
||||
Only keys not set by the user are filled in. For known tags, a preset unit or
|
||||
device class is dropped if it is incompatible with the one set by the user.
|
||||
Unknown tags get the legacy defaults, merged key by key as before.
|
||||
"""
|
||||
if CONF_TAG_NAME not in config:
|
||||
return config
|
||||
|
||||
# Runs before cv.string, so the raw value may not be a string yet
|
||||
tag_name = str(config[CONF_TAG_NAME])
|
||||
preset = next(
|
||||
(p for prefix, p in TIC_TAG_CONFIGS.items() if tag_name.startswith(prefix)),
|
||||
None,
|
||||
)
|
||||
|
||||
config = dict(config)
|
||||
if preset is None:
|
||||
for key, value in _LEGACY_DEFAULT.items():
|
||||
config.setdefault(key, value)
|
||||
return config
|
||||
|
||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset[CONF_UNIT_OF_MEASUREMENT])
|
||||
device_class = config.get(CONF_DEVICE_CLASS, preset[CONF_DEVICE_CLASS])
|
||||
compatible = unit in _COMPATIBLE_UNITS.get(device_class, ())
|
||||
|
||||
for key, value in preset.items():
|
||||
if key in config:
|
||||
continue
|
||||
if key in (CONF_UNIT_OF_MEASUREMENT, CONF_DEVICE_CLASS) and not compatible:
|
||||
continue
|
||||
config[key] = value
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
apply_tag_config,
|
||||
sensor.sensor_schema(
|
||||
TeleInfoSensor,
|
||||
accuracy_decimals=0,
|
||||
).extend(TELEINFO_LISTENER_SCHEMA),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID])
|
||||
cg.add(teleinfo.register_teleinfo_listener(var))
|
||||
|
||||
@@ -116,8 +116,7 @@ void TeleInfo::loop() {
|
||||
* 0xa | Tag | 0x9 | Timestamp | 0x9 | 0x9 | CRC | 0xd
|
||||
*
|
||||
*/
|
||||
while ((buf_finger = static_cast<char *>(memchr(buf_finger, (int) 0xa, buf_index_ - 1))) &&
|
||||
((buf_finger - buf_) < buf_index_)) { // NOLINT(clang-diagnostic-sign-compare)
|
||||
while ((buf_finger = static_cast<char *>(memchr(buf_finger, (int) 0xa, buf_end - buf_finger)))) {
|
||||
/*
|
||||
* Make sure timesamp is nullified between each tag as some tags don't
|
||||
* have a timestamp
|
||||
@@ -168,8 +167,8 @@ void TeleInfo::loop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Advance buf_finger to end of group */
|
||||
buf_finger += field_len + 1 + 1 + 1;
|
||||
/* Resume after the group's 0xd; grp_end < buf_end, so this stays in bounds. */
|
||||
buf_finger = grp_end + 1;
|
||||
|
||||
publish_value_(std::string(tag_), std::string(val_));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ MODELS = {
|
||||
"RAC-PT1411HWRU-C": Model.MODEL_RAC_PT1411HWRU_C,
|
||||
"RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F,
|
||||
"RAS-2819T": Model.MODEL_RAS_2819T,
|
||||
"RAS-B10N3KV2": Model.MODEL_RAS_B10N3KV2,
|
||||
"SEIYA": Model.MODEL_SEIYA,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).extend(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "toshiba.h"
|
||||
#include "esphome/components/remote_base/toshiba_ac_protocol.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <vector>
|
||||
@@ -163,6 +164,54 @@ const uint8_t RAS_2819T_AUTO_DRY_FAN_BYTE = 0x65;
|
||||
const uint8_t RAS_2819T_AUTO_DRY_SUFFIX = 0x3A;
|
||||
const uint8_t RAS_2819T_HEAT_SUFFIX = 0x3B;
|
||||
|
||||
// Bit timings differ per protocol family; the frame layout is shared
|
||||
struct ToshibaTimings {
|
||||
uint16_t header_mark;
|
||||
uint16_t header_space;
|
||||
uint16_t bit_mark;
|
||||
uint16_t zero_space;
|
||||
uint16_t one_space;
|
||||
uint16_t gap_space;
|
||||
bool gap_after_last; // Seiya ends on a bare mark and only puts the gap between repeats
|
||||
};
|
||||
// Kept in flash on ESP8266, where plain const objects referenced through a function would land in RAM
|
||||
static const ToshibaTimings TOSHIBA_TIMINGS PROGMEM = {TOSHIBA_HEADER_MARK,
|
||||
TOSHIBA_HEADER_SPACE,
|
||||
TOSHIBA_BIT_MARK,
|
||||
TOSHIBA_ZERO_SPACE,
|
||||
TOSHIBA_ONE_SPACE,
|
||||
TOSHIBA_GAP_SPACE,
|
||||
true};
|
||||
static const ToshibaTimings SEIYA_TIMINGS PROGMEM = {4630, 4450, 625, 490, 1570, 5830, false};
|
||||
// The RAS-B13 remote family does not accept the generic timings; these are IRremoteESP8266's TOSHIBA_AC values
|
||||
static const ToshibaTimings RAS_B10N3KV2_TIMINGS PROGMEM = {4400, 4300, 580, 490, 1600, 7400, true};
|
||||
|
||||
static ToshibaTimings timings_for(Model model) {
|
||||
const ToshibaTimings *source = &TOSHIBA_TIMINGS;
|
||||
switch (model) {
|
||||
case MODEL_SEIYA:
|
||||
source = &SEIYA_TIMINGS;
|
||||
break;
|
||||
case MODEL_RAS_B10N3KV2:
|
||||
source = &RAS_B10N3KV2_TIMINGS;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ToshibaTimings timings;
|
||||
progmem_memcpy(&timings, source, sizeof(timings));
|
||||
return timings;
|
||||
}
|
||||
|
||||
// Seiya is the generic frame plus two bytes: a feature marker and a one-shot swing command
|
||||
static constexpr uint8_t SEIYA_MESSAGE_LENGTH = 11;
|
||||
static constexpr uint8_t SEIYA_FEATURE_MARKER = 0x21;
|
||||
static constexpr uint8_t SEIYA_SWING_BOTH = 0x01;
|
||||
static constexpr uint8_t SEIYA_SWING_OMNI = 0x02; // no ESPHome equivalent; treated as BOTH
|
||||
static constexpr uint8_t SEIYA_SWING_VERTICAL = 0x08;
|
||||
static constexpr uint8_t SEIYA_SWING_HORIZONTAL = 0x09;
|
||||
static constexpr uint8_t SEIYA_SWING_NEUTRAL = 0x17; // no swing change in this frame
|
||||
|
||||
// RAS-2819T temperature codes for 18-30°C
|
||||
static const uint8_t RAS_2819T_TEMP_CODES[] = {
|
||||
0x10, // 18°C
|
||||
@@ -404,12 +453,15 @@ void ToshibaClimate::setup() {
|
||||
this->maximum_temperature_ = this->temperature_max_();
|
||||
this->swing_modes_ = this->toshiba_swing_modes_();
|
||||
|
||||
// Ensure swing mode is always initialized to a valid value
|
||||
// Ensure swing mode is always initialized to a valid value. OFF also stands for "not yet
|
||||
// commanded" on models that cannot switch the swing off, such as Seiya.
|
||||
if (this->swing_modes_.empty() || !this->swing_modes_.count(this->swing_mode)) {
|
||||
// No swing support for this model or current swing mode not supported, reset to OFF
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
|
||||
// A restored swing mode is already in effect, so it must not go out as a fresh swing command
|
||||
this->last_swing_mode_ = this->swing_mode;
|
||||
|
||||
// Ensure mode is valid - ESPHome should only use standard climate modes
|
||||
if (this->mode != climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_HEAT &&
|
||||
this->mode != climate::CLIMATE_MODE_COOL && this->mode != climate::CLIMATE_MODE_HEAT_COOL &&
|
||||
@@ -451,8 +503,9 @@ void ToshibaClimate::transmit_state() {
|
||||
}
|
||||
|
||||
void ToshibaClimate::transmit_generic_() {
|
||||
const bool seiya = this->model_ == MODEL_SEIYA;
|
||||
uint8_t message[16] = {0};
|
||||
uint8_t message_length = 9;
|
||||
const uint8_t message_length = seiya ? SEIYA_MESSAGE_LENGTH : 9;
|
||||
|
||||
// Header
|
||||
message[0] = 0xf2;
|
||||
@@ -473,6 +526,40 @@ void ToshibaClimate::transmit_generic_() {
|
||||
message[5] = (temperature - static_cast<uint8_t>(TOSHIBA_GENERIC_TEMP_C_MIN)) << 4;
|
||||
|
||||
// Mode and fan
|
||||
message[6] = this->encode_mode_fan_();
|
||||
|
||||
// Zero
|
||||
message[7] = 0x00;
|
||||
|
||||
if (seiya) {
|
||||
// Captured OFF frames carry no fan bits
|
||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||
message[6] = TOSHIBA_MODE_OFF;
|
||||
}
|
||||
message[8] = SEIYA_FEATURE_MARKER;
|
||||
// Swing is a momentary command, sent only when the mode changes, like the remote does
|
||||
message[9] = SEIYA_SWING_NEUTRAL;
|
||||
if (this->swing_mode != this->last_swing_mode_) {
|
||||
message[9] = this->seiya_swing_code_();
|
||||
this->last_swing_mode_ = this->swing_mode;
|
||||
}
|
||||
}
|
||||
|
||||
// The last byte is the xor of all bytes from [4]
|
||||
for (uint8_t i = 4; i < message_length - 1; i++) {
|
||||
message[message_length - 1] ^= message[i];
|
||||
}
|
||||
|
||||
// Transmit
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
|
||||
this->encode_(data, message, message_length, 1);
|
||||
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
uint8_t ToshibaClimate::encode_mode_fan_() const {
|
||||
uint8_t mode;
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
@@ -523,27 +610,20 @@ void ToshibaClimate::transmit_generic_() {
|
||||
fan = TOSHIBA_FAN_SPEED_AUTO;
|
||||
break;
|
||||
}
|
||||
message[6] = fan | mode;
|
||||
return fan | mode;
|
||||
}
|
||||
|
||||
// Zero
|
||||
message[7] = 0x00;
|
||||
|
||||
// If timers bit in the command is set, two extra bytes are added here
|
||||
|
||||
// If power bit is set in the command, one extra byte is added here
|
||||
|
||||
// The last byte is the xor of all bytes from [4]
|
||||
for (uint8_t i = 4; i < 8; i++) {
|
||||
message[8] ^= message[i];
|
||||
uint8_t ToshibaClimate::seiya_swing_code_() const {
|
||||
switch (this->swing_mode) {
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
return SEIYA_SWING_VERTICAL;
|
||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
||||
return SEIYA_SWING_HORIZONTAL;
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
return SEIYA_SWING_BOTH;
|
||||
default:
|
||||
return SEIYA_SWING_NEUTRAL;
|
||||
}
|
||||
|
||||
// Transmit
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
|
||||
this->encode_(data, message, message_length, 1);
|
||||
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
void ToshibaClimate::transmit_rac_pt1411hwru_() {
|
||||
@@ -1085,14 +1165,16 @@ bool ToshibaClimate::process_ras_2819t_command_(const remote_base::ToshibaAcData
|
||||
|
||||
bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
// Try modern ToshibaAcProtocol decoder first (handles RAS-2819T and potentially others)
|
||||
remote_base::ToshibaAcProtocol toshiba_protocol;
|
||||
auto decode_result = toshiba_protocol.decode(data);
|
||||
if (this->model_ != MODEL_SEIYA) {
|
||||
remote_base::ToshibaAcProtocol toshiba_protocol;
|
||||
auto decode_result = toshiba_protocol.decode(data);
|
||||
|
||||
if (decode_result.has_value()) {
|
||||
auto toshiba_data = decode_result.value();
|
||||
// Validate and process RAS-2819T commands
|
||||
if (is_valid_ras_2819t_command(toshiba_data.rc_code_1, toshiba_data.rc_code_2)) {
|
||||
return this->process_ras_2819t_command_(toshiba_data);
|
||||
if (decode_result.has_value()) {
|
||||
auto toshiba_data = decode_result.value();
|
||||
// Validate and process RAS-2819T commands
|
||||
if (is_valid_ras_2819t_command(toshiba_data.rc_code_1, toshiba_data.rc_code_2)) {
|
||||
return this->process_ras_2819t_command_(toshiba_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1101,7 +1183,8 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
uint8_t message_length = TOSHIBA_HEADER_LENGTH, temperature_code = 0;
|
||||
|
||||
// Validate header
|
||||
if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) {
|
||||
const ToshibaTimings timings = timings_for(this->model_);
|
||||
if (!data.expect_item(timings.header_mark, timings.header_space)) {
|
||||
return false;
|
||||
}
|
||||
// Read incoming bits into buffer
|
||||
@@ -1117,6 +1200,10 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
return false;
|
||||
} else {
|
||||
// First checksum was valid so continue receiving the remaining bits
|
||||
// The shortest frame is 9 bytes (length byte 3); longer ones must still fit the buffer
|
||||
if (message[2] < 3 || message[2] > sizeof(message) - TOSHIBA_HEADER_LENGTH - 2) {
|
||||
return false;
|
||||
}
|
||||
message_length = message[2] + 2;
|
||||
}
|
||||
// Decode the remaining bytes
|
||||
@@ -1276,59 +1363,11 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the mode
|
||||
switch (message[6] & 0x0F) {
|
||||
case TOSHIBA_MODE_OFF:
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
break;
|
||||
|
||||
case TOSHIBA_MODE_COOL:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
|
||||
case TOSHIBA_MODE_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
|
||||
case TOSHIBA_MODE_FAN_ONLY:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
|
||||
case TOSHIBA_MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
|
||||
case TOSHIBA_MODE_AUTO:
|
||||
default:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
this->decode_mode_fan_temperature_(message);
|
||||
// The marker byte tells a Seiya frame from another checksum-valid 11 byte frame within receiver tolerance
|
||||
if (this->model_ == MODEL_SEIYA && message_length == SEIYA_MESSAGE_LENGTH && message[8] == SEIYA_FEATURE_MARKER) {
|
||||
this->seiya_decode_swing_(message[9]);
|
||||
}
|
||||
|
||||
// Get the fan mode
|
||||
switch (message[6] & 0xF0) {
|
||||
case TOSHIBA_FAN_SPEED_QUIET:
|
||||
this->fan_mode = climate::CLIMATE_FAN_QUIET;
|
||||
break;
|
||||
|
||||
case TOSHIBA_FAN_SPEED_1:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
|
||||
case TOSHIBA_FAN_SPEED_3:
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
break;
|
||||
|
||||
case TOSHIBA_FAN_SPEED_5:
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
|
||||
case TOSHIBA_FAN_SPEED_AUTO:
|
||||
default:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the target temperature
|
||||
this->target_temperature = (message[5] >> 4) + TOSHIBA_GENERIC_TEMP_C_MIN;
|
||||
}
|
||||
|
||||
this->publish_state();
|
||||
@@ -1337,31 +1376,36 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
|
||||
void ToshibaClimate::encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, const uint8_t nbytes,
|
||||
const uint8_t repeat) {
|
||||
const ToshibaTimings timings = timings_for(this->model_);
|
||||
data->set_carrier_frequency(TOSHIBA_CARRIER_FREQUENCY);
|
||||
|
||||
for (uint8_t copy = 0; copy <= repeat; copy++) {
|
||||
data->item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE);
|
||||
data->item(timings.header_mark, timings.header_space);
|
||||
|
||||
for (uint8_t byte = 0; byte < nbytes; byte++) {
|
||||
for (uint8_t bit = 0; bit < 8; bit++) {
|
||||
data->mark(TOSHIBA_BIT_MARK);
|
||||
data->mark(timings.bit_mark);
|
||||
if (message[byte] & (1 << (7 - bit))) {
|
||||
data->space(TOSHIBA_ONE_SPACE);
|
||||
data->space(timings.one_space);
|
||||
} else {
|
||||
data->space(TOSHIBA_ZERO_SPACE);
|
||||
data->space(timings.zero_space);
|
||||
}
|
||||
}
|
||||
}
|
||||
data->item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE);
|
||||
data->mark(timings.bit_mark);
|
||||
if (copy < repeat || timings.gap_after_last) {
|
||||
data->space(timings.gap_space);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ToshibaClimate::decode_(remote_base::RemoteReceiveData *data, uint8_t *message, const uint8_t nbytes) {
|
||||
const ToshibaTimings timings = timings_for(this->model_);
|
||||
for (uint8_t byte = 0; byte < nbytes; byte++) {
|
||||
for (uint8_t bit = 0; bit < 8; bit++) {
|
||||
if (data->expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ONE_SPACE)) {
|
||||
if (data->expect_item(timings.bit_mark, timings.one_space)) {
|
||||
message[byte] |= 1 << (7 - bit);
|
||||
} else if (data->expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ZERO_SPACE)) {
|
||||
} else if (data->expect_item(timings.bit_mark, timings.zero_space)) {
|
||||
message[byte] &= static_cast<uint8_t>(~(1 << (7 - bit)));
|
||||
} else {
|
||||
return false;
|
||||
@@ -1371,4 +1415,80 @@ bool ToshibaClimate::decode_(remote_base::RemoteReceiveData *data, uint8_t *mess
|
||||
return true;
|
||||
}
|
||||
|
||||
void ToshibaClimate::decode_mode_fan_temperature_(const uint8_t *message) {
|
||||
switch (message[6] & 0x0F) {
|
||||
case TOSHIBA_MODE_OFF:
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
break;
|
||||
|
||||
case TOSHIBA_MODE_COOL:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
|
||||
case TOSHIBA_MODE_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
|
||||
case TOSHIBA_MODE_FAN_ONLY:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
|
||||
case TOSHIBA_MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
|
||||
case TOSHIBA_MODE_AUTO:
|
||||
default:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
}
|
||||
|
||||
// Speeds 2 and 4 are not exposed; report the nearest mode
|
||||
switch (message[6] & 0xF0) {
|
||||
case TOSHIBA_FAN_SPEED_QUIET:
|
||||
this->fan_mode = climate::CLIMATE_FAN_QUIET;
|
||||
break;
|
||||
|
||||
case TOSHIBA_FAN_SPEED_1:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
|
||||
case TOSHIBA_FAN_SPEED_2:
|
||||
case TOSHIBA_FAN_SPEED_3:
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
break;
|
||||
|
||||
case TOSHIBA_FAN_SPEED_4:
|
||||
case TOSHIBA_FAN_SPEED_5:
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
|
||||
case TOSHIBA_FAN_SPEED_AUTO:
|
||||
default:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
}
|
||||
|
||||
this->target_temperature = (message[5] >> 4) + TOSHIBA_GENERIC_TEMP_C_MIN;
|
||||
}
|
||||
|
||||
void ToshibaClimate::seiya_decode_swing_(uint8_t code) {
|
||||
// A momentary command: the neutral value on an ordinary update means no change, not swing off
|
||||
switch (code) {
|
||||
case SEIYA_SWING_BOTH:
|
||||
case SEIYA_SWING_OMNI:
|
||||
this->swing_mode = climate::CLIMATE_SWING_BOTH;
|
||||
break;
|
||||
case SEIYA_SWING_VERTICAL:
|
||||
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||
break;
|
||||
case SEIYA_SWING_HORIZONTAL:
|
||||
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
// Keep the tracker in sync so the received command is not sent back out
|
||||
this->last_swing_mode_ = this->swing_mode;
|
||||
}
|
||||
|
||||
} // namespace esphome::toshiba
|
||||
|
||||
@@ -11,6 +11,8 @@ enum Model {
|
||||
MODEL_RAC_PT1411HWRU_C = 1, // Temperature range is from 16 to 30
|
||||
MODEL_RAC_PT1411HWRU_F = 2, // Temperature range is from 16 to 30
|
||||
MODEL_RAS_2819T = 3, // RAS-2819T protocol variant, temperature range 18 to 30
|
||||
MODEL_SEIYA = 4, // Seiya family (RAS-B13E2KVG-E etc.), generic frame with a swing byte
|
||||
MODEL_RAS_B10N3KV2 = 5, // RAS-B13 remote family, generic frame with IRremoteESP8266's TOSHIBA_AC timings
|
||||
};
|
||||
|
||||
// Supported temperature ranges
|
||||
@@ -50,7 +52,7 @@ class ToshibaClimate final : public climate_ir::ClimateIR {
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
|
||||
private:
|
||||
// RAS-2819T state tracking for swing mode optimization
|
||||
// Last swing mode sent or received, for models whose swing is a one-shot command
|
||||
climate::ClimateSwingMode last_swing_mode_{climate::CLIMATE_SWING_OFF};
|
||||
climate::ClimateMode last_mode_{climate::CLIMATE_MODE_OFF};
|
||||
optional<climate::ClimateFanMode> last_fan_mode_{};
|
||||
@@ -71,13 +73,24 @@ class ToshibaClimate final : public climate_ir::ClimateIR {
|
||||
return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models
|
||||
}
|
||||
climate::ClimateSwingModeMask toshiba_swing_modes_() {
|
||||
return (this->model_ == MODEL_GENERIC)
|
||||
? climate::ClimateSwingModeMask()
|
||||
: climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL};
|
||||
if (this->model_ == MODEL_GENERIC || this->model_ == MODEL_RAS_B10N3KV2)
|
||||
return climate::ClimateSwingModeMask();
|
||||
if (this->model_ == MODEL_SEIYA) {
|
||||
// No captured code stops the swing, so OFF is not offered; the remote only selects a direction
|
||||
return climate::ClimateSwingModeMask{climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_BOTH};
|
||||
}
|
||||
return climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL};
|
||||
}
|
||||
void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat);
|
||||
bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes);
|
||||
|
||||
// Shared by every model that uses the F2 0D frame
|
||||
uint8_t encode_mode_fan_() const;
|
||||
void decode_mode_fan_temperature_(const uint8_t *message);
|
||||
uint8_t seiya_swing_code_() const;
|
||||
void seiya_decode_swing_(uint8_t code);
|
||||
|
||||
Model model_;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from esphome.components import time, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "tuya"
|
||||
@@ -13,6 +14,7 @@ CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints"
|
||||
CONF_ON_DATAPOINT_UPDATE = "on_datapoint_update"
|
||||
CONF_DATAPOINT_TYPE = "datapoint_type"
|
||||
CONF_STATUS_PIN = "status_pin"
|
||||
CONF_FORCE_TIME_SYNC = "force_time_sync"
|
||||
|
||||
tuya_ns = cg.esphome_ns.namespace("tuya")
|
||||
TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True)
|
||||
@@ -43,11 +45,23 @@ DATAPOINT_TYPES = {
|
||||
|
||||
|
||||
CONF_TUYA_ID = "tuya_id"
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
def _validate_time_options(config: ConfigType) -> ConfigType:
|
||||
if config[CONF_FORCE_TIME_SYNC] and CONF_TIME_ID not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_FORCE_TIME_SYNC} requires {CONF_TIME_ID}",
|
||||
path=[CONF_FORCE_TIME_SYNC],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Tuya),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Optional(CONF_FORCE_TIME_SYNC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS): cv.ensure_list(
|
||||
cv.uint8_t
|
||||
),
|
||||
@@ -63,7 +77,8 @@ CONFIG_SCHEMA = (
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA),
|
||||
_validate_time_options,
|
||||
)
|
||||
|
||||
|
||||
@@ -71,9 +86,11 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
if CONF_TIME_ID in config:
|
||||
time_ = await cg.get_variable(config[CONF_TIME_ID])
|
||||
if (time_id := config.get(CONF_TIME_ID)) is not None:
|
||||
time_ = await cg.get_variable(time_id)
|
||||
cg.add(var.set_time_id(time_))
|
||||
if config[CONF_FORCE_TIME_SYNC]:
|
||||
cg.add(var.set_force_time_sync(True))
|
||||
if CONF_STATUS_PIN in config:
|
||||
status_pin_ = await cg.gpio_pin_expression(config[CONF_STATUS_PIN])
|
||||
cg.add(var.set_status_pin(status_pin_))
|
||||
|
||||
@@ -23,11 +23,13 @@ ESPHOME_LOG_TAG(TAG, "tuya");
|
||||
static constexpr uint32_t HEARTBEAT_INTERVAL_ID = 0;
|
||||
static constexpr uint32_t WIFI_STATUS_INTERVAL_ID = 1;
|
||||
static constexpr uint32_t DATAPOINT_DUMP_TIMEOUT_ID = 2;
|
||||
static constexpr uint32_t FORCE_TIME_SYNC_INTERVAL_ID = 3;
|
||||
static const int COMMAND_DELAY = 10;
|
||||
static const int RECEIVE_TIMEOUT = 300;
|
||||
static const int MAX_RETRIES = 5;
|
||||
// Max bytes to log for datapoint values (larger values are truncated)
|
||||
static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16;
|
||||
static constexpr uint32_t FORCE_TIME_SYNC_CHECK_INTERVAL = 1000;
|
||||
|
||||
static bool network_is_connected() {
|
||||
#ifdef USE_NETWORK
|
||||
@@ -39,6 +41,13 @@ static bool network_is_connected() {
|
||||
|
||||
void Tuya::setup() {
|
||||
this->set_interval(HEARTBEAT_INTERVAL_ID, 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
|
||||
#ifdef USE_TIME
|
||||
if (this->force_time_sync_) {
|
||||
this->register_local_time_sync_callback_();
|
||||
this->set_interval(FORCE_TIME_SYNC_INTERVAL_ID, FORCE_TIME_SYNC_CHECK_INTERVAL,
|
||||
[this] { this->check_force_time_sync_(); });
|
||||
}
|
||||
#endif
|
||||
if (this->status_pin_ != nullptr) {
|
||||
this->status_pin_->digital_write(false);
|
||||
}
|
||||
@@ -99,6 +108,11 @@ void Tuya::dump_config() {
|
||||
}
|
||||
LOG_PIN(" Status Pin: ", this->status_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str());
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Force time sync: %s", YESNO(this->force_time_sync_));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Tuya::validate_message_() {
|
||||
@@ -306,12 +320,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_ != nullptr) {
|
||||
this->send_local_time_();
|
||||
|
||||
if (!this->time_sync_callback_registered_) {
|
||||
// tuya mcu supports time, so we let them know when our time changed
|
||||
this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); });
|
||||
this->time_sync_callback_registered_ = true;
|
||||
}
|
||||
this->register_local_time_sync_callback_();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
@@ -616,7 +625,23 @@ void Tuya::send_wifi_status_() {
|
||||
}
|
||||
|
||||
#ifdef USE_TIME
|
||||
void Tuya::send_local_time_() {
|
||||
void Tuya::register_local_time_sync_callback_() {
|
||||
if (this->time_sync_callback_registered_) {
|
||||
return;
|
||||
}
|
||||
this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); });
|
||||
this->time_sync_callback_registered_ = true;
|
||||
}
|
||||
|
||||
void Tuya::check_force_time_sync_() {
|
||||
ESPTime now = this->time_id_->now();
|
||||
if (!now.is_valid() || now.minute == this->last_force_time_sync_minute_)
|
||||
return;
|
||||
this->last_force_time_sync_minute_ = now.minute;
|
||||
this->send_local_time_(true);
|
||||
}
|
||||
|
||||
void Tuya::send_local_time_(bool periodic) {
|
||||
std::vector<uint8_t> payload;
|
||||
ESPTime now = this->time_id_->now();
|
||||
if (now.is_valid()) {
|
||||
@@ -631,7 +656,12 @@ void Tuya::send_local_time_() {
|
||||
if (day_of_week == 0) {
|
||||
day_of_week = 7;
|
||||
}
|
||||
ESP_LOGD(TAG, "Sending local time");
|
||||
// The forced send repeats every minute, so it stays out of the debug log
|
||||
if (periodic) {
|
||||
ESP_LOGV(TAG, "Sending local time");
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Sending local time");
|
||||
}
|
||||
payload = std::vector<uint8_t>{0x01, year, month, day_of_month, hour, minute, second, day_of_week};
|
||||
} else {
|
||||
// By spec we need to notify MCU that the time was not obtained if this is a response to a query
|
||||
|
||||
@@ -113,6 +113,7 @@ class Tuya final : public Component, public uart::UARTDevice {
|
||||
TuyaInitState get_init_state();
|
||||
#ifdef USE_TIME
|
||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||
void set_force_time_sync(bool force_time_sync) { this->force_time_sync_ = force_time_sync; }
|
||||
#endif
|
||||
void add_ignore_mcu_update_on_datapoints(uint8_t ignore_mcu_update_on_datapoints) {
|
||||
this->ignore_mcu_update_on_datapoints_.push_back(ignore_mcu_update_on_datapoints);
|
||||
@@ -144,9 +145,13 @@ class Tuya final : public Component, public uart::UARTDevice {
|
||||
uint8_t get_wifi_rssi_();
|
||||
|
||||
#ifdef USE_TIME
|
||||
void send_local_time_();
|
||||
void register_local_time_sync_callback_();
|
||||
void check_force_time_sync_();
|
||||
void send_local_time_(bool periodic = false);
|
||||
void send_gmt_time_();
|
||||
time::RealTimeClock *time_id_{nullptr};
|
||||
bool force_time_sync_{false};
|
||||
uint8_t last_force_time_sync_minute_{0xFF}; // 0xFF: no minute seen yet
|
||||
bool time_sync_callback_registered_{false};
|
||||
bool gmt_time_sync_callback_registered_{false};
|
||||
#endif
|
||||
|
||||
@@ -13,7 +13,14 @@ from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32S31,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERNAL, CONF_MODEL, CONF_NAME, CONF_ON_START
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INTERNAL,
|
||||
CONF_MODEL,
|
||||
CONF_NAME,
|
||||
CONF_ON_START,
|
||||
CONF_POLLING_INTERVAL,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -25,6 +32,7 @@ from .const import (
|
||||
CONF_POWER_SOURCE,
|
||||
CONF_REPORT,
|
||||
CONF_ROUTER,
|
||||
CONF_SLEEPY,
|
||||
CONF_USE_DEVICE_TYPE,
|
||||
CONF_WIPE_ON_BOOT,
|
||||
KEY_ZIGBEE,
|
||||
@@ -35,7 +43,6 @@ from .const import (
|
||||
from .const_zephyr import (
|
||||
CONF_IEEE802154_VENDOR_OUI,
|
||||
CONF_MAX_EP_NUMBER_ZEPHYR,
|
||||
CONF_SLEEPY,
|
||||
CONF_ZIGBEE_ID,
|
||||
KEY_EP_NUMBER,
|
||||
)
|
||||
@@ -128,8 +135,11 @@ NUMBER_SCHEMA = (
|
||||
|
||||
|
||||
def _validate_router_sleepy(config: ConfigType) -> ConfigType:
|
||||
if config.get(CONF_ROUTER) and config.get(CONF_SLEEPY):
|
||||
raise cv.Invalid("router and sleepy are mutually exclusive")
|
||||
if config.get(CONF_ROUTER):
|
||||
if config.get(CONF_SLEEPY):
|
||||
raise cv.Invalid("router and sleepy are mutually exclusive")
|
||||
if config.get(CONF_POLLING_INTERVAL):
|
||||
raise cv.Invalid(f"{CONF_POLLING_INTERVAL} is only valid for end devices.")
|
||||
return config
|
||||
|
||||
|
||||
@@ -160,9 +170,17 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.requires_component("nrf52"),
|
||||
),
|
||||
cv.OnlyWith(CONF_SLEEPY, "nrf52", default=False): cv.All(
|
||||
cv.Optional(CONF_SLEEPY, default=False): cv.All(
|
||||
cv.boolean,
|
||||
),
|
||||
cv.Optional(CONF_POLLING_INTERVAL): cv.All(
|
||||
cv.requires_component(ESP32_DOMAIN),
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(
|
||||
min=cv.TimePeriodMilliseconds(milliseconds=200),
|
||||
max=cv.TimePeriodMinutes(minutes=60), # 1 hour
|
||||
),
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_validate_router_sleepy,
|
||||
|
||||
@@ -72,6 +72,7 @@ CONF_WIPE_ON_BOOT = "wipe_on_boot"
|
||||
CONF_REPORT = "report"
|
||||
CONF_ROUTER = "router"
|
||||
CONF_POWER_SOURCE = "power_source"
|
||||
CONF_SLEEPY = "sleepy"
|
||||
CONF_USE_DEVICE_TYPE = "use_device_type"
|
||||
POWER_SOURCE = {
|
||||
"UNKNOWN": 0x00, # ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN
|
||||
|
||||
@@ -4,7 +4,6 @@ CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor"
|
||||
CONF_ZIGBEE_SENSOR = "zigbee_sensor"
|
||||
CONF_ZIGBEE_SWITCH = "zigbee_switch"
|
||||
CONF_ZIGBEE_NUMBER = "zigbee_number"
|
||||
CONF_SLEEPY = "sleepy"
|
||||
CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui"
|
||||
|
||||
# Keys for CORE.data storage
|
||||
|
||||
@@ -319,7 +319,7 @@ void ZigbeeComponent::setup() {
|
||||
#else
|
||||
esp_zigbee_zed_config_s zb_zed_cfg = {
|
||||
.ed_timeout = EZB_NWK_ED_TIMEOUT_64MIN,
|
||||
.keep_alive = ED_KEEP_ALIVE,
|
||||
.keep_alive = this->keep_alive_,
|
||||
};
|
||||
device_config.zed_config = zb_zed_cfg;
|
||||
#endif
|
||||
@@ -339,6 +339,8 @@ void ZigbeeComponent::setup() {
|
||||
|
||||
#ifdef CONFIG_ZB_ZCZR
|
||||
ezb_bdb_set_router_rejoin_required(true);
|
||||
#else
|
||||
ezb_nwk_set_rx_on_when_idle(!this->sleepy_); // if sleepy, disable RX when idle
|
||||
#endif
|
||||
|
||||
ezb_aps_secur_enable_distributed_security(false);
|
||||
@@ -419,18 +421,27 @@ void ZigbeeComponent::loop() {
|
||||
|
||||
void ZigbeeComponent::dump_config() {
|
||||
if (esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Zigbee\n"
|
||||
" Model: %.*s\n"
|
||||
" Router: %s\n"
|
||||
" Device is joined to the network: %s\n"
|
||||
" Current channel: %d\n"
|
||||
" Short addr: 0x%04X\n"
|
||||
" Short pan id: 0x%04X",
|
||||
this->basic_cluster_data_.model[0],
|
||||
reinterpret_cast<const char *>(this->basic_cluster_data_.model + 1),
|
||||
YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER), YESNO(ezb_bdb_dev_joined()),
|
||||
ezb_nwk_get_current_channel(), ezb_nwk_get_short_address(), ezb_nwk_get_panid());
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
"Zigbee\n"
|
||||
" Model: %.*s\n"
|
||||
#ifdef CONFIG_ZB_ZCZR
|
||||
" Router: %s\n"
|
||||
#else
|
||||
" Sleepy end device: %s\n"
|
||||
" Poll interval: %" PRIu32 " ms\n"
|
||||
#endif
|
||||
" Device is joined to the network: %s\n"
|
||||
" Current channel: %d\n"
|
||||
" Short addr: 0x%04X\n"
|
||||
" Short pan id: 0x%04X",
|
||||
this->basic_cluster_data_.model[0], reinterpret_cast<const char *>(this->basic_cluster_data_.model + 1),
|
||||
#ifdef CONFIG_ZB_ZCZR
|
||||
YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER),
|
||||
#else
|
||||
YESNO(this->sleepy_), this->keep_alive_,
|
||||
#endif
|
||||
YESNO(ezb_bdb_dev_joined()), ezb_nwk_get_current_channel(), ezb_nwk_get_short_address(), ezb_nwk_get_panid());
|
||||
esp_zigbee_lock_release();
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
namespace esphome::zigbee {
|
||||
|
||||
/* Zigbee configuration */
|
||||
static const uint16_t ED_KEEP_ALIVE = 3000; /* 3000 millisecond */
|
||||
static const uint8_t MAX_CHILDREN = 10;
|
||||
static const uint32_t EZB_PRIMARY_CHANNEL_MASK = 0x07FFF800U; /* channels 11-26 */
|
||||
static constexpr uint8_t MAX_ZB_QUEUE_SIZE = 32;
|
||||
@@ -43,6 +42,8 @@ class ZigbeeComponent final : public Component {
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_sleepy(bool sleepy) { this->sleepy_ = sleepy; }
|
||||
void set_keep_alive(uint32_t keep_alive) { this->keep_alive_ = keep_alive; }
|
||||
void set_basic_cluster(const char *model, const char *manufacturer, uint8_t power_source);
|
||||
void add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role);
|
||||
void create_default_cluster(uint8_t endpoint_id, uint16_t device_id);
|
||||
@@ -73,6 +74,8 @@ class ZigbeeComponent final : public Component {
|
||||
bool is_joined() { return this->joined_; }
|
||||
|
||||
protected:
|
||||
bool sleepy_{false};
|
||||
uint32_t keep_alive_{3000};
|
||||
struct {
|
||||
uint8_t *model;
|
||||
uint8_t *manufacturer;
|
||||
|
||||
@@ -24,6 +24,7 @@ from esphome.const import (
|
||||
CONF_MAX_LENGTH,
|
||||
CONF_MODEL,
|
||||
CONF_NAME,
|
||||
CONF_POLLING_INTERVAL,
|
||||
CONF_TYPE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE,
|
||||
@@ -45,6 +46,7 @@ from .const import (
|
||||
CONF_POWER_SOURCE,
|
||||
CONF_REPORT,
|
||||
CONF_ROUTER,
|
||||
CONF_SLEEPY,
|
||||
CONF_USE_DEVICE_TYPE,
|
||||
KEY_ZIGBEE,
|
||||
POWER_SOURCE,
|
||||
@@ -412,6 +414,11 @@ async def esp32_to_code(config: ConfigType) -> "MockObj":
|
||||
# setup zigbee components
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
if config.get(CONF_SLEEPY):
|
||||
cg.add(var.set_sleepy(True))
|
||||
if (keep_alive := config.get(CONF_POLLING_INTERVAL, 3000)) != 3000:
|
||||
cg.add(var.set_keep_alive(keep_alive))
|
||||
|
||||
cg.add(
|
||||
var.set_basic_cluster(
|
||||
config[CONF_MODEL],
|
||||
|
||||
@@ -24,6 +24,7 @@ from .const import (
|
||||
BACNET_UNITS,
|
||||
CONF_POWER_SOURCE,
|
||||
CONF_ROUTER,
|
||||
CONF_SLEEPY,
|
||||
CONF_WIPE_ON_BOOT,
|
||||
KEY_ZIGBEE,
|
||||
POWER_SOURCE,
|
||||
@@ -35,7 +36,6 @@ from .const import (
|
||||
)
|
||||
from .const_zephyr import (
|
||||
CONF_IEEE802154_VENDOR_OUI,
|
||||
CONF_SLEEPY,
|
||||
CONF_ZIGBEE_BINARY_SENSOR,
|
||||
CONF_ZIGBEE_ID,
|
||||
CONF_ZIGBEE_NUMBER,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user