mirror of
https://github.com/esphome/esphome.git
synced 2026-10-26 16:02:13 +00:00
Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f228b21746 | ||
|
|
970885b79b | ||
|
|
db6c956e55 | ||
|
|
c95ce587c0 | ||
|
|
3540ac4d1f | ||
|
|
180d7b31d8 | ||
|
|
3cf79df27c | ||
|
|
2beb2de8a1 | ||
|
|
9e457b11ba | ||
|
|
ee41748744 | ||
|
|
08d4d59b1f | ||
|
|
e4c0ef7c95 | ||
|
|
26ee68652f | ||
|
|
fa88c9218b | ||
|
|
44e27c08d8 | ||
|
|
f431cdfb0c | ||
|
|
ba488eab6e | ||
|
|
f21183c160 | ||
|
|
60ca75baad | ||
|
|
b668cb1e50 | ||
|
|
ab7d0ca17d | ||
|
|
43f10a7cfe | ||
|
|
d5e755e407 | ||
|
|
cf67610d43 | ||
|
|
bd55bdcd6c | ||
|
|
f42d1ac645 | ||
|
|
5198c37cd9 | ||
|
|
d9fcc292e2 | ||
|
|
8f8e615ee7 | ||
|
|
f91ad8d43c | ||
|
|
465a8b0386 | ||
|
|
9e692b6772 | ||
|
|
1fd4025a0e | ||
|
|
03b8bd7941 | ||
|
|
0e2c906fce | ||
|
|
d260265cf5 | ||
|
|
2388c3313d | ||
|
|
e717a21bce | ||
|
|
d47a510b84 | ||
|
|
a16d0c48a0 | ||
|
|
8541104d87 | ||
|
|
579c5a5226 | ||
|
|
a027785fbf | ||
|
|
d7f5976525 | ||
|
|
4c8de264fd | ||
|
|
73564dbebd | ||
|
|
4165dbb534 | ||
|
|
3bc9f74f46 | ||
|
|
0dd27c5d08 | ||
|
|
84b0f35b91 | ||
|
|
2aa3677bbf | ||
|
|
c2fbcf0f24 | ||
|
|
afbd2ab7b1 | ||
|
|
3c65c55df0 | ||
|
|
7704ad9384 | ||
|
|
50fdc8e85b | ||
|
|
3e6324445b | ||
|
|
e0e5cbdc27 | ||
|
|
870b80e463 | ||
|
|
899e88be72 |
No files matched your search
@@ -217,9 +217,10 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
|
||||
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
|
||||
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
|
||||
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
|
||||
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
|
||||
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
|
||||
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
|
||||
`if (!this->status_comm_result(ok)) return;` sets or clears the warning. A child sensor that skips repeated
|
||||
values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
|
||||
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
|
||||
end of `setup()` when the entities need a state at boot.
|
||||
@@ -443,12 +444,16 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
uses `button.ApplyButton` (`switch.ApplySwitch`, `number.ApplyNumber`, `select.ApplySelect`) in its schema
|
||||
and `apply=` on the `sub_*` call. The body is a call on the hub with `entity` and `state`, `value` or
|
||||
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state.
|
||||
`publish="momentary"` is only for legacy action switches (restart, factory reset): it publishes off and runs
|
||||
the body only when turned on. Never use it for a new entity; an action is a button.
|
||||
```python
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(CONF_BLUETOOTH, None, apply="set_bluetooth(state)", publish="after")
|
||||
```
|
||||
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
|
||||
to still drive a state when the restore mode is disabled.
|
||||
A hub that applies its switches' restored states itself reads `sw->get_initial_write_state()`, which
|
||||
already applies `inverted`.
|
||||
|
||||
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
|
||||
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
|
||||
|
||||
@@ -2233,6 +2233,12 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def command_rotate_key(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome.cli.rotate_key import command_rotate_key as run
|
||||
|
||||
return run(args, config)
|
||||
|
||||
|
||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome.cli.rename import command_rename as run
|
||||
|
||||
@@ -2259,6 +2265,7 @@ POST_CONFIG_ACTIONS = {
|
||||
"clean-mqtt": command_clean_mqtt,
|
||||
"idedata": command_idedata,
|
||||
"rename": command_rename,
|
||||
"rotate-key": command_rotate_key,
|
||||
"discover": command_discover,
|
||||
"analyze-memory": command_analyze_memory,
|
||||
"bundle": command_bundle,
|
||||
@@ -2656,6 +2663,37 @@ def parse_args(argv):
|
||||
)
|
||||
parser_rename.add_argument("name", help="The new name for the device.", type=str)
|
||||
|
||||
parser_rotate_key = subparsers.add_parser(
|
||||
"rotate-key",
|
||||
help=(
|
||||
"Change the OTA encryption key over the air: write a new key to the "
|
||||
"yaml, build with it, upload with the current key, confirm."
|
||||
),
|
||||
parents=[mqtt_options],
|
||||
)
|
||||
parser_rotate_key.add_argument(
|
||||
"configuration", help="Your YAML configuration file.", nargs=1
|
||||
)
|
||||
parser_rotate_key.add_argument(
|
||||
"--device",
|
||||
action="append",
|
||||
help=ARGUMENT_HELP_DEVICE,
|
||||
)
|
||||
parser_rotate_key.add_argument(
|
||||
"--prompt-new-key",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Ask for the new key instead of generating one; read without echo, "
|
||||
"or as one line from stdin when that is not a terminal."
|
||||
),
|
||||
)
|
||||
parser_rotate_key.add_argument(
|
||||
"-y",
|
||||
"--yes",
|
||||
action="store_true",
|
||||
help="Do not ask for confirmation when the api encryption key changes too.",
|
||||
)
|
||||
|
||||
parser_analyze_memory = subparsers.add_parser(
|
||||
"analyze-memory",
|
||||
help="Analyze memory usage by component.",
|
||||
|
||||
+30
-13
@@ -7,6 +7,7 @@ import re
|
||||
import textwrap
|
||||
|
||||
from esphome.build_helpers import pch
|
||||
from esphome.build_helpers.ccache import CCACHE_PCH_MIN_VERSION
|
||||
from esphome.components.esp32 import (
|
||||
KEY_SRC_CMAKE,
|
||||
get_esp32_variant,
|
||||
@@ -515,7 +516,7 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||
# Where CMake puts the .gch of the src component; ccache reads the checksum
|
||||
# next to it in place of the .gch
|
||||
_PCH_SUM_PATH = "build/esp-idf/src/CMakeFiles/__idf_src.dir/cmake_pch.hxx.gch.sum"
|
||||
# Where the Windows gate records its choice
|
||||
# Where the CMake gate records its choice
|
||||
_PCH_CHOICE_VAR = "ESPHOME_PCH"
|
||||
|
||||
|
||||
@@ -531,16 +532,32 @@ def _pch_cmake_block() -> str:
|
||||
block = f"""target_precompile_headers(${{COMPONENT_LIB}} PRIVATE
|
||||
{headers}
|
||||
)"""
|
||||
if not pch.pch_needs_gcc_check():
|
||||
if pch.pch_forced():
|
||||
return f"\n# ESPHome precompiled header\n{block}\n"
|
||||
# Before the first configure only CMake knows the compiler version
|
||||
return f"""
|
||||
# ESPHome precompiled header, unless GCC bug 14940 keeps it from loading
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND ({pch.PCH_WINDOWS_CMAKE_OLD_GCC}))
|
||||
# Only CMake knows the compiler version before the first configure, and
|
||||
# the ccache IDF found is the one the build runs
|
||||
gcc_gate = ""
|
||||
if pch.pch_needs_gcc_check():
|
||||
gcc_gate = f"""if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND ({pch.PCH_WINDOWS_CMAKE_OLD_GCC}))
|
||||
message(STATUS "ESPHome: GCC ${{CMAKE_CXX_COMPILER_VERSION}} cannot load a precompiled header on Windows; compiling without it")
|
||||
set({_PCH_CHOICE_VAR} OFF CACHE BOOL "ESPHome precompiled header in use" FORCE)
|
||||
else()
|
||||
set({_PCH_CHOICE_VAR} ON CACHE BOOL "ESPHome precompiled header in use" FORCE)
|
||||
set(_esphome_pch OFF)
|
||||
endif()
|
||||
"""
|
||||
min_ccache = ".".join(map(str, CCACHE_PCH_MIN_VERSION))
|
||||
return f"""
|
||||
# ESPHome precompiled header, unless a check below rules it out
|
||||
set(_esphome_pch ON)
|
||||
{gcc_gate}if(CCACHE_ENABLE AND CCACHE_FOUND)
|
||||
execute_process(COMMAND "${{CCACHE_FOUND}}" --version OUTPUT_VARIABLE _esphome_ccache ERROR_QUIET)
|
||||
if(_esphome_ccache MATCHES "ccache version ([0-9.]+)")
|
||||
if(CMAKE_MATCH_1 VERSION_LESS {min_ccache})
|
||||
message(STATUS "ESPHome: ccache ${{CMAKE_MATCH_1}} can reuse stale objects with a precompiled header; compiling without it (upgrade ccache to {min_ccache} or newer, or set ESPHOME_PCH_ENABLE=1 to force)")
|
||||
set(_esphome_pch OFF)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
set({_PCH_CHOICE_VAR} ${{_esphome_pch}} CACHE BOOL "ESPHome precompiled header in use" FORCE)
|
||||
if(_esphome_pch)
|
||||
{textwrap.indent(block, " ")}
|
||||
endif()
|
||||
"""
|
||||
@@ -557,10 +574,10 @@ def write_pch_checksum() -> None:
|
||||
|
||||
if not pch.pch_enabled():
|
||||
return
|
||||
# The gate's choice, cached by configure
|
||||
if pch.pch_needs_gcc_check() and get_cmake_cache_value(_PCH_CHOICE_VAR) != "ON":
|
||||
return
|
||||
pch.log_pch_in_use()
|
||||
# Written whatever the gate chose last time: a build tree without that
|
||||
# choice, or a reconfigure that turns the pch on, must not see a stale sum
|
||||
if pch.pch_forced() or get_cmake_cache_value(_PCH_CHOICE_VAR) == "ON":
|
||||
pch.log_pch_in_use()
|
||||
checksum = pch.pch_checksum(
|
||||
CORE.relative_src_path(),
|
||||
pch.PCH_DEFAULT_HEADERS,
|
||||
|
||||
@@ -3,21 +3,68 @@ resolution, and default ``CCACHE_*`` values."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
|
||||
from esphome.framework_helpers import (
|
||||
strip_win_long_path_prefix,
|
||||
tool_version_probe,
|
||||
tool_version_runs,
|
||||
)
|
||||
from esphome.helpers import FALSY_ENV_STRINGS, TRUTHY_ENV_STRINGS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Before 4.7 ccache stores a pch consumer whose header is newer than one
|
||||
# second without that header, so a later build reuses it after the header
|
||||
# changed (#21621)
|
||||
CCACHE_PCH_MIN_VERSION = (4, 7)
|
||||
|
||||
_VERSION_RE = re.compile(r"ccache version (\d+(?:\.\d+)*)")
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _ccache_probe(ccache: str) -> str | Exception:
|
||||
"""``ccache --version``, run once per binary per process."""
|
||||
return tool_version_probe(ccache)
|
||||
|
||||
|
||||
def _ccache_runs(ccache: str) -> bool:
|
||||
"""Return True when the ``ccache`` found on PATH actually runs."""
|
||||
return tool_version_runs(
|
||||
ccache,
|
||||
"Ignoring ccache at %s because it failed to run; compiling without ccache",
|
||||
probe=_ccache_probe,
|
||||
)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _ccache_too_old_for_pch(ccache: str) -> bool:
|
||||
output = _ccache_probe(ccache)
|
||||
if not isinstance(output, str) or (found := _VERSION_RE.search(output)) is None:
|
||||
return False
|
||||
if tuple(map(int, found[1].split("."))) >= CCACHE_PCH_MIN_VERSION:
|
||||
return False
|
||||
_LOGGER.info(
|
||||
"ccache %s at %s can reuse stale objects with a precompiled header; "
|
||||
"compiling without it (upgrade ccache to %s or newer, or set "
|
||||
"ESPHOME_PCH_ENABLE=1 to force)",
|
||||
found[1],
|
||||
ccache,
|
||||
".".join(map(str, CCACHE_PCH_MIN_VERSION)),
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def ccache_breaks_pch() -> bool:
|
||||
"""Whether the ccache this build uses is too old for a precompiled header.
|
||||
ESP-IDF builds check the ccache IDF finds in CMake instead (build_gen.espidf)."""
|
||||
return (ccache := resolve_ccache_path()) is not None and _ccache_too_old_for_pch(
|
||||
ccache
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
|
||||
from esphome.build_helpers.ccache import (
|
||||
ccache_breaks_pch,
|
||||
effective_ccache_basedir,
|
||||
parse_enable_env,
|
||||
)
|
||||
from esphome.const import PLATFORM_NRF52
|
||||
from esphome.helpers import write_file_if_changed
|
||||
|
||||
@@ -117,9 +121,16 @@ def pch_needs_gcc_check() -> bool:
|
||||
return sys.platform == "win32" and parse_enable_env("ESPHOME_PCH_ENABLE") is None
|
||||
|
||||
|
||||
def _ccache_rules_out_pch() -> bool:
|
||||
"""An old ccache, unless ``ESPHOME_PCH_ENABLE=1`` forces the pch on.
|
||||
ESP-IDF runs the same check in CMake (build_gen.espidf)."""
|
||||
return not pch_forced() and ccache_breaks_pch()
|
||||
|
||||
|
||||
def pch_usable(cxx: Sequence[Path | str]) -> bool:
|
||||
"""The knob plus the host rule; ``ESPHOME_PCH_ENABLE=1`` skips the rule."""
|
||||
if not pch_enabled():
|
||||
"""The knob plus the ccache and host rules; ``ESPHOME_PCH_ENABLE=1``
|
||||
skips the rules."""
|
||||
if not pch_enabled() or _ccache_rules_out_pch():
|
||||
return False
|
||||
if not pch_needs_gcc_check():
|
||||
return True
|
||||
@@ -162,7 +173,11 @@ def pch_script_enabled() -> bool:
|
||||
"""Whether this PlatformIO build takes the pch script."""
|
||||
from esphome.core import CORE
|
||||
|
||||
return pch_enabled() and CORE.target_platform not in PCH_SCRIPT_EXCLUDED_PLATFORMS
|
||||
return (
|
||||
pch_enabled()
|
||||
and CORE.target_platform not in PCH_SCRIPT_EXCLUDED_PLATFORMS
|
||||
and not _ccache_rules_out_pch()
|
||||
)
|
||||
|
||||
|
||||
def pch_header_text(include_headers: Iterable[str]) -> str:
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
"""``esphome rotate-key``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from esphome import espota2
|
||||
from esphome.components.noise import (
|
||||
generate_encryption_key,
|
||||
static_encryption_key,
|
||||
validate_encryption_key,
|
||||
)
|
||||
from esphome.config_validation import Invalid
|
||||
from esphome.const import CONF_API, CONF_PASSWORD, CONF_PORT
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.espota2 import ProbeResult
|
||||
from esphome.log import AnsiFore, color
|
||||
from esphome.ota_key_edit import locate_key_edits, old_key_edit, with_sharers
|
||||
from esphome.types import ConfigType
|
||||
from esphome.upload_targets import PortType, get_port_type
|
||||
from esphome.util import (
|
||||
ESPHOME_COMMAND,
|
||||
read_secret_line,
|
||||
run_external_process,
|
||||
safe_input,
|
||||
safe_print,
|
||||
)
|
||||
from esphome.yaml_edit import RestoreError, Snapshot, apply_line_edits, restore_files
|
||||
|
||||
PRECHECK_TIMEOUT = 15.0
|
||||
# What to do when the device does not accept the key in the yaml before anything is written
|
||||
PRECHECK_ADVICE = {
|
||||
ProbeResult.UNREACHABLE: (
|
||||
"The device could not be reached; check that it is online, then try again."
|
||||
),
|
||||
ProbeResult.REJECTED: (
|
||||
"The device runs a different key than the yaml. Install the current "
|
||||
"configuration first, presenting its key with --prompt-ota-key, then rotate."
|
||||
),
|
||||
ProbeResult.NOT_OFFERED: (
|
||||
"The device does not offer encryption: it runs a firmware older than "
|
||||
"ESPHome 2026.9.0 or without a key. Install the current configuration "
|
||||
"first, then rotate."
|
||||
),
|
||||
ProbeResult.DEVICE_ERROR: (
|
||||
"The device answered with an error (see the warning above); fix that first."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def command_rotate_key(args: argparse.Namespace, config: ConfigType) -> int | None:
|
||||
"""Write a new key, build with it, upload with the current one, confirm."""
|
||||
from esphome.__main__ import (
|
||||
Purpose,
|
||||
_esphome_ota_conf,
|
||||
_resolve_network_devices,
|
||||
choose_upload_log_host,
|
||||
)
|
||||
|
||||
def fail(message: str) -> int:
|
||||
safe_print(color(AnsiFore.BOLD_RED, message))
|
||||
return 1
|
||||
|
||||
ota_conf = _esphome_ota_conf(config)
|
||||
if ota_conf is None:
|
||||
return fail("rotate-key needs the esphome OTA platform in the configuration")
|
||||
old_key = static_encryption_key(ota_conf)
|
||||
if old_key is None:
|
||||
return fail(
|
||||
"rotate-key needs 'encryption:' under the esphome OTA platform with a "
|
||||
"key in the yaml; a key provisioned at runtime is rotated from Home "
|
||||
"Assistant"
|
||||
)
|
||||
if CORE.is_host:
|
||||
return fail("rotate-key is for devices updated over the air")
|
||||
old_key = str(old_key)
|
||||
remote_port = int(ota_conf[CONF_PORT])
|
||||
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device, check_default=None, purpose=Purpose.UPLOADING
|
||||
)
|
||||
if get_port_type(devices[0]) in (PortType.SERIAL, PortType.BOOTSEL):
|
||||
return fail(
|
||||
"rotate-key works over the air; on a serial connection change the "
|
||||
"key in the yaml and run 'esphome run' instead"
|
||||
)
|
||||
network_devices = _resolve_network_devices(devices, config, args)
|
||||
|
||||
if args.prompt_new_key:
|
||||
new_key = read_secret_line("New OTA encryption key: ")
|
||||
else:
|
||||
new_key = generate_encryption_key()
|
||||
try:
|
||||
validate_encryption_key(new_key)
|
||||
except Invalid as err:
|
||||
return fail(f"Invalid new key: {err}")
|
||||
if new_key == old_key:
|
||||
return fail("The new key is the same as the current one")
|
||||
|
||||
try:
|
||||
edits = locate_key_edits(old_key, new_key)
|
||||
# The previous key stays in the config as old_key so an install still
|
||||
# reaches the device if it ends up running either key
|
||||
edits += old_key_edit(old_key)
|
||||
unchecked = with_sharers(edits)
|
||||
except EsphomeError as err:
|
||||
return fail(str(err))
|
||||
|
||||
warnings = []
|
||||
# With the recommended shape the ota inherits the api key, so rotating
|
||||
# it changes what Home Assistant connects with
|
||||
if str(static_encryption_key(config.get(CONF_API) or {})) == old_key:
|
||||
warnings.append(
|
||||
"The api encryption key changes too; Home Assistant will ask for "
|
||||
"the new key after the install."
|
||||
)
|
||||
shared: dict[str, set[str]] = {}
|
||||
for edit in edits:
|
||||
what = f"secret '{edit.secret}'" if edit.secret else edit.path.name
|
||||
for path in edit.shared_with:
|
||||
shared.setdefault(
|
||||
str(path.relative_to(CORE.config_dir.resolve())), set()
|
||||
).add(what)
|
||||
if unchecked:
|
||||
warnings.append(
|
||||
"Could not check every configuration for shared use: "
|
||||
+ "; ".join(unchecked)
|
||||
)
|
||||
if shared:
|
||||
warnings.append(
|
||||
"Other configurations share what is rewritten: "
|
||||
+ "; ".join(
|
||||
f"{p} ({', '.join(sorted(w))})" for p, w in sorted(shared.items())
|
||||
)
|
||||
+ ". Their devices keep the previous key until their next install; "
|
||||
"make sure those configurations carry it as old_key."
|
||||
)
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
safe_print(color(AnsiFore.BOLD_YELLOW, warning))
|
||||
if not args.yes:
|
||||
if not sys.stdin.isatty():
|
||||
return fail("Confirm with --yes when there is no terminal")
|
||||
if safe_input("Continue? [y/N] ").strip().lower() not in ("y", "yes"):
|
||||
return 1
|
||||
|
||||
safe_print("Checking that the device accepts the current key...")
|
||||
found = espota2.probe_ota_key(
|
||||
network_devices,
|
||||
remote_port,
|
||||
old_key,
|
||||
timeout=PRECHECK_TIMEOUT,
|
||||
retry_rejected=False,
|
||||
)
|
||||
if found is not ProbeResult.ACCEPTED:
|
||||
return fail(PRECHECK_ADVICE[found])
|
||||
|
||||
# Both keys are shown only when a file could not be put back
|
||||
keys = f"Previous key: {old_key}\nNew key: {new_key}"
|
||||
try:
|
||||
originals = apply_line_edits(edits)
|
||||
except RestoreError as err:
|
||||
safe_print(keys)
|
||||
return fail(str(err))
|
||||
except EsphomeError as err:
|
||||
return fail(str(err))
|
||||
except KeyboardInterrupt as err:
|
||||
if notes := getattr(err, "__notes__", None): # a rollback that failed too
|
||||
safe_print(keys)
|
||||
return fail("Interrupted; " + "; ".join(notes))
|
||||
return fail("Interrupted before the upload")
|
||||
# From here every exit restores or reports, so nothing sits outside the try
|
||||
uploaded = False
|
||||
try:
|
||||
safe_print(
|
||||
"Wrote the new key to "
|
||||
+ ", ".join(color(AnsiFore.CYAN, str(p)) for p in originals)
|
||||
)
|
||||
# Global options go before the subcommand; the compile parser is strict
|
||||
cli_args = ["--dashboard"] if CORE.dashboard else []
|
||||
if args.toolchain:
|
||||
cli_args += ["--toolchain", str(args.toolchain)]
|
||||
for key, value in args.substitution or []:
|
||||
cli_args += ["-s", key, value]
|
||||
cli_args += ["compile", str(CORE.config_path)]
|
||||
if run_external_process(*ESPHOME_COMMAND, *cli_args) != 0:
|
||||
return fail("Compiling with the new key failed")
|
||||
|
||||
def image_sent() -> None:
|
||||
# The device accepted the session and may commit from here on;
|
||||
# the edited config reaches it on either key
|
||||
nonlocal uploaded
|
||||
uploaded = True
|
||||
|
||||
rc, _ = espota2.run_ota(
|
||||
network_devices,
|
||||
remote_port,
|
||||
ota_conf.get(CONF_PASSWORD),
|
||||
CORE.firmware_bin,
|
||||
noise_psk=old_key,
|
||||
plaintext_fallback=False,
|
||||
on_upload_start=image_sent,
|
||||
)
|
||||
if rc != 0:
|
||||
return fail(
|
||||
"Uploading with the current key failed. The configuration keeps "
|
||||
"the new key with the previous one as old_key, so the next "
|
||||
"install reaches the device whichever key it runs."
|
||||
if uploaded
|
||||
else "The upload did not reach the device."
|
||||
)
|
||||
safe_print("Waiting for the device to come back with the new key...")
|
||||
found = espota2.probe_ota_key(network_devices, remote_port, new_key)
|
||||
if found is not ProbeResult.ACCEPTED:
|
||||
return fail(
|
||||
(
|
||||
"The device still answers with the previous key."
|
||||
if found is ProbeResult.REJECTED
|
||||
else "The device did not come back to confirm the new key."
|
||||
)
|
||||
+ " The configuration keeps the new key, with the previous one as "
|
||||
"old_key, so the next install reaches the device either way."
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
return fail(
|
||||
"Interrupted; the device's key was not confirmed"
|
||||
if uploaded
|
||||
else "Interrupted before the upload"
|
||||
)
|
||||
finally:
|
||||
# Before the upload nothing changed on the device; after it the device
|
||||
# most likely runs the new key, and old_key covers the other case
|
||||
if uploaded:
|
||||
safe_print(f"New OTA encryption key: {color(AnsiFore.CYAN, new_key)}")
|
||||
else:
|
||||
_restore(originals, keys)
|
||||
|
||||
safe_print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
|
||||
return 0
|
||||
|
||||
|
||||
def _restore(originals: dict[Path, Snapshot], keys: str) -> None:
|
||||
"""Put the previous key back, and drop the images a build made with the
|
||||
new key; both keys are shown when a step fails."""
|
||||
files = ", ".join(str(p) for p in originals)
|
||||
try:
|
||||
restore_files(originals)
|
||||
except (EsphomeError, KeyboardInterrupt) as err:
|
||||
# An interrupted restore carries its report as a note
|
||||
why = str(err) or "; ".join(getattr(err, "__notes__", ()))
|
||||
safe_print(color(AnsiFore.BOLD_RED, why or f"Interrupted restoring {files}"))
|
||||
safe_print(keys)
|
||||
else:
|
||||
safe_print(color(AnsiFore.BOLD_YELLOW, f"Restored the previous key in {files}"))
|
||||
build_dir = CORE.firmware_bin.parent
|
||||
failed: list[str] = []
|
||||
images: list[Path] = []
|
||||
try:
|
||||
images = [p for s in (".bin", ".uf2") for p in build_dir.glob(f"*{s}")]
|
||||
for image in images: # every one is tried, whatever the others did
|
||||
try:
|
||||
image.unlink()
|
||||
except OSError as err:
|
||||
failed.append(f"{image}: {err}")
|
||||
except (OSError, KeyboardInterrupt) as err:
|
||||
failed.append(f"{build_dir}: {str(err) or 'interrupted'}")
|
||||
if failed:
|
||||
safe_print(
|
||||
color(
|
||||
AnsiFore.BOLD_RED,
|
||||
"Could not remove images that may hold the new key: "
|
||||
+ "; ".join(failed),
|
||||
)
|
||||
)
|
||||
safe_print(keys)
|
||||
elif images:
|
||||
safe_print(
|
||||
color(
|
||||
AnsiFore.BOLD_YELLOW,
|
||||
f"Removed the images in {build_dir}, which may hold the new key; "
|
||||
"compile again before the next upload.",
|
||||
)
|
||||
)
|
||||
@@ -33,9 +33,35 @@ _CACHE_VERSION = 1
|
||||
_LAMBDA_KEY = "__esphome_lambda__"
|
||||
|
||||
|
||||
_CACHE_SUFFIX = ".validated.json"
|
||||
|
||||
|
||||
def compiled_config_path(config_filename: str) -> Path:
|
||||
"""Path to the cached validated config alongside the storage sidecar."""
|
||||
return CORE.data_dir / "storage" / f"{config_filename}.validated.json"
|
||||
return CORE.data_dir / "storage" / f"{config_filename}{_CACHE_SUFFIX}"
|
||||
|
||||
|
||||
def invalidate_compiled_config() -> None:
|
||||
"""Drop every cache in the storage dir after a change to a file the
|
||||
mtime check cannot see, such as secrets.yaml or an include; other
|
||||
configurations may share that file."""
|
||||
storage = compiled_config_path(CORE.config_filename).parent
|
||||
try:
|
||||
caches = [p for p in storage.iterdir() if p.name.endswith(_CACHE_SUFFIX)]
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except OSError as err:
|
||||
raise EsphomeError(
|
||||
f"Could not list the validated config caches in {storage}: {err}"
|
||||
) from err
|
||||
for path in caches:
|
||||
try:
|
||||
path.unlink(missing_ok=True)
|
||||
except OSError as err:
|
||||
# A stale cache would present the old key on the next upload
|
||||
raise EsphomeError(
|
||||
f"Could not remove the validated config cache {path}: {err}"
|
||||
) from err
|
||||
|
||||
|
||||
def save_compiled_config(config: ConfigType) -> None:
|
||||
|
||||
@@ -90,6 +90,10 @@ class AirtonClimate : public climate_ir::ClimateIR {
|
||||
bool set_display_state(bool state);
|
||||
bool set_vertical_direction_state(VerticalDirection direction);
|
||||
void send_settings();
|
||||
void control_vertical_direction(VerticalDirection direction) {
|
||||
if (this->set_vertical_direction_state(direction))
|
||||
this->send_settings();
|
||||
}
|
||||
void set_setting(AirtonSetting setting, bool state) {
|
||||
const bool changed = setting == AirtonSetting::AIRTON_SETTING_DISPLAY ? this->set_display_state(state)
|
||||
: this->set_sleep_mode_state(state);
|
||||
|
||||
@@ -9,18 +9,16 @@ from ..climate import (
|
||||
CONF_VERTICAL_DIRECTION,
|
||||
VERTICAL_DIRECTIONS,
|
||||
AirtonClimate,
|
||||
airton_ns,
|
||||
VerticalDirection,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@Procsiab"]
|
||||
|
||||
VerticalDirectionSelect = airton_ns.class_("VerticalDirectionSelect", select.Select)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_AIRTON_ID): cv.use_id(AirtonClimate),
|
||||
cv.Optional(CONF_VERTICAL_DIRECTION): select.select_schema(
|
||||
VerticalDirectionSelect,
|
||||
select.ApplySelect,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
),
|
||||
}
|
||||
@@ -34,4 +32,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
CONF_VERTICAL_DIRECTION,
|
||||
hub.set_vertical_direction_select,
|
||||
options=list(VERTICAL_DIRECTIONS),
|
||||
# The options are in the order of the enum; the hub publishes the state
|
||||
apply=f"control_vertical_direction(static_cast<{VerticalDirection}>(index))",
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
#include "../airton.h"
|
||||
|
||||
namespace esphome::airton {
|
||||
|
||||
class VerticalDirectionSelect : public select::Select, public Parented<AirtonClimate> {
|
||||
protected:
|
||||
// The options are in the order of the enum, so the index is the direction; the parent publishes the state
|
||||
void control(size_t index) override {
|
||||
if (this->parent_->set_vertical_direction_state(static_cast<VerticalDirection>(index)))
|
||||
this->parent_->send_settings();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::airton
|
||||
@@ -11,7 +11,52 @@ ESPHOME_LOG_TAG(TAG, "apds9960");
|
||||
this->mark_failed(LOG_STR("Setting up APDS9960 registers failed")); \
|
||||
return; \
|
||||
}
|
||||
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
|
||||
|
||||
static constexpr i2c::I2CRegValue APDS9960_TIMING[] PROGMEM = {
|
||||
// ATime (ADC integration time, 2.78ms increments, 0x81) -> 0xDB (103ms)
|
||||
{0x81, 0xDB},
|
||||
// WTime (Wait time, 0x83) -> 0xF6 (27ms)
|
||||
{0x83, 0xF6},
|
||||
// PPulse (0x8E) -> 0x87 (16us, 8 pulses)
|
||||
{0x8E, 0x87},
|
||||
// POffset UR (0x9D) -> 0 (no offset)
|
||||
{0x9D, 0x00},
|
||||
// POffset DL (0x9E) -> 0 (no offset)
|
||||
{0x9E, 0x00},
|
||||
// Config 1 (0x8D) -> 0x60 (no wtime factor)
|
||||
{0x8D, 0x60},
|
||||
};
|
||||
|
||||
static constexpr i2c::I2CRegValue APDS9960_INTERRUPT_GESTURE[] PROGMEM = {
|
||||
// Pers (0x8C) -> 0x11 (2 consecutive proximity or ALS for interrupt)
|
||||
{0x8C, 0x11},
|
||||
// Config 2 (0x90) -> 0x01 (no saturation interrupts or LED boost)
|
||||
{0x90, 0x01},
|
||||
// Config 3 (0x9F) -> 0x00 (enable all photodiodes, no SAI)
|
||||
{0x9F, 0x00},
|
||||
// GPenTh (0xA0, gesture enter threshold) -> 0x28 (also 0x32)
|
||||
{0xA0, 0x28},
|
||||
// GPexTh (0xA1, gesture exit threshold) -> 0x1E
|
||||
{0xA1, 0x1E},
|
||||
// GConf 1 (0xA2, gesture config 1) -> 0x40 (4 gesture events for interrupt (GFIFO 3), 1 for exit)
|
||||
{0xA2, 0x40},
|
||||
};
|
||||
|
||||
static constexpr i2c::I2CRegValue APDS9960_GESTURE_OFFSETS[] PROGMEM = {
|
||||
// GOffsetU (0xA4) -> 0x00 (no offset)
|
||||
{0xA4, 0x00},
|
||||
// GOffsetD (0xA5) -> 0x00 (no offset)
|
||||
{0xA5, 0x00},
|
||||
// GOffsetL (0xA7) -> 0x00 (no offset)
|
||||
{0xA7, 0x00},
|
||||
// GOffsetR (0xA9) -> 0x00 (no offset)
|
||||
{0xA9, 0x00},
|
||||
// GPulse (0xA6) -> 0xC9 (32 µs, 10 pulses)
|
||||
{0xA6, 0xC9},
|
||||
// GConf 3 (0xAA, gesture config 3) -> 0x00 (all photodiodes active during gesture, all gesture dimensions enabled)
|
||||
// 0x00 -> all dimensions, 0x01 -> up down, 0x02 -> left right
|
||||
{0xAA, 0x00},
|
||||
};
|
||||
|
||||
void APDS9960::setup() {
|
||||
uint8_t id;
|
||||
@@ -25,83 +70,27 @@ void APDS9960::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
// ATime (ADC integration time, 2.78ms increments, 0x81) -> 0xDB (103ms)
|
||||
APDS9960_WRITE_BYTE(0x81, 0xDB);
|
||||
// WTime (Wait time, 0x83) -> 0xF6 (27ms)
|
||||
APDS9960_WRITE_BYTE(0x83, 0xF6);
|
||||
// PPulse (0x8E) -> 0x87 (16us, 8 pulses)
|
||||
APDS9960_WRITE_BYTE(0x8E, 0x87);
|
||||
// POffset UR (0x9D) -> 0 (no offset)
|
||||
APDS9960_WRITE_BYTE(0x9D, 0x00);
|
||||
// POffset DL (0x9E) -> 0 (no offset)
|
||||
APDS9960_WRITE_BYTE(0x9E, 0x00);
|
||||
// Config 1 (0x8D) -> 0x60 (no wtime factor)
|
||||
APDS9960_WRITE_BYTE(0x8D, 0x60);
|
||||
APDS9960_ERROR_CHECK(this->write_registers(APDS9960_TIMING));
|
||||
|
||||
// Control (0x8F) ->
|
||||
uint8_t val = 0;
|
||||
APDS9960_ERROR_CHECK(this->read_byte(0x8F, &val));
|
||||
val &= 0b00111111;
|
||||
// led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA
|
||||
val |= (this->led_drive_ & 0b11) << 6;
|
||||
// Control (0x8F): led drive (0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA),
|
||||
// proximity gain (0 -> 1x, 1 -> 2X, 2 -> 4X, 3 -> 8X), ambient light gain (0 -> 1x, 1 -> 4x, 2 -> 16x, 3 -> 64x)
|
||||
APDS9960_ERROR_CHECK(this->update_register(
|
||||
0x8F, 0b11001111,
|
||||
(this->led_drive_ & 0b11) << 6 | (this->proximity_gain_ & 0b11) << 2 | (this->ambient_gain_ & 0b11)));
|
||||
|
||||
val &= 0b11110011;
|
||||
// proximity gain, 0 -> 1x, 1 -> 2X, 2 -> 4X, 3 -> 8X
|
||||
val |= (this->proximity_gain_ & 0b11) << 2;
|
||||
APDS9960_ERROR_CHECK(this->write_registers(APDS9960_INTERRUPT_GESTURE));
|
||||
|
||||
val &= 0b11111100;
|
||||
// ambient light gain, 0 -> 1x, 1 -> 4x, 2 -> 16x, 3 -> 64x
|
||||
val |= (this->ambient_gain_ & 0b11) << 0;
|
||||
APDS9960_WRITE_BYTE(0x8F, val);
|
||||
// GConf 2 (0xA3): gesture gain (0 -> 1x, 1 -> 2x, 2 -> 4x, 3 -> 8x),
|
||||
// gesture led drive (0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA), gesture wait time
|
||||
// (0 -> 0ms, 1 -> 2.8ms, 2 -> 5.6ms, 3 -> 8.4ms, 4 -> 14.0ms, 5 -> 22.4 ms, 6 -> 30.8ms, 7 -> 39.2 ms)
|
||||
APDS9960_ERROR_CHECK(this->update_register(
|
||||
0xA3, 0b01111111,
|
||||
(this->gesture_gain_ & 0b11) << 5 | (this->gesture_led_drive_ & 0b11) << 3 | (this->gesture_wait_time_ & 0b111)));
|
||||
|
||||
// Pers (0x8C) -> 0x11 (2 consecutive proximity or ALS for interrupt)
|
||||
APDS9960_WRITE_BYTE(0x8C, 0x11);
|
||||
// Config 2 (0x90) -> 0x01 (no saturation interrupts or LED boost)
|
||||
APDS9960_WRITE_BYTE(0x90, 0x01);
|
||||
// Config 3 (0x9F) -> 0x00 (enable all photodiodes, no SAI)
|
||||
APDS9960_WRITE_BYTE(0x9F, 0x00);
|
||||
// GPenTh (0xA0, gesture enter threshold) -> 0x28 (also 0x32)
|
||||
APDS9960_WRITE_BYTE(0xA0, 0x28);
|
||||
// GPexTh (0xA1, gesture exit threshold) -> 0x1E
|
||||
APDS9960_WRITE_BYTE(0xA1, 0x1E);
|
||||
|
||||
// GConf 1 (0xA2, gesture config 1) -> 0x40 (4 gesture events for interrupt (GFIFO 3), 1 for exit)
|
||||
APDS9960_WRITE_BYTE(0xA2, 0x40);
|
||||
|
||||
// GConf 2 (0xA3, gesture config 2) ->
|
||||
APDS9960_ERROR_CHECK(this->read_byte(0xA3, &val));
|
||||
val &= 0b10011111;
|
||||
// gesture gain, 0 -> 1x, 1 -> 2x, 2 -> 4x, 3 -> 8x
|
||||
val |= (this->gesture_gain_ & 0b11) << 5;
|
||||
|
||||
val &= 0b11100111;
|
||||
// gesture led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA
|
||||
val |= (this->gesture_led_drive_ & 0b11) << 3;
|
||||
|
||||
val &= 0b11111000;
|
||||
// gesture wait time
|
||||
// 0 -> 0ms, 1 -> 2.8ms, 2 -> 5.6ms, 3 -> 8.4ms
|
||||
// 4 -> 14.0ms, 5 -> 22.4 ms, 6 -> 30.8ms, 7 -> 39.2 ms
|
||||
val |= (this->gesture_wait_time_ & 0b111) << 0;
|
||||
APDS9960_WRITE_BYTE(0xA3, val);
|
||||
|
||||
// GOffsetU (0xA4) -> 0x00 (no offset)
|
||||
APDS9960_WRITE_BYTE(0xA4, 0x00);
|
||||
// GOffsetD (0xA5) -> 0x00 (no offset)
|
||||
APDS9960_WRITE_BYTE(0xA5, 0x00);
|
||||
// GOffsetL (0xA7) -> 0x00 (no offset)
|
||||
APDS9960_WRITE_BYTE(0xA7, 0x00);
|
||||
// GOffsetR (0xA9) -> 0x00 (no offset)
|
||||
APDS9960_WRITE_BYTE(0xA9, 0x00);
|
||||
// GPulse (0xA6) -> 0xC9 (32 µs, 10 pulses)
|
||||
APDS9960_WRITE_BYTE(0xA6, 0xC9);
|
||||
|
||||
// GConf 3 (0xAA, gesture config 3) -> 0x00 (all photodiodes active during gesture, all gesture dimensions enabled)
|
||||
// 0x00 -> all dimensions, 0x01 -> up down, 0x02 -> left right
|
||||
APDS9960_WRITE_BYTE(0xAA, 0x00);
|
||||
APDS9960_ERROR_CHECK(this->write_registers(APDS9960_GESTURE_OFFSETS));
|
||||
|
||||
// Enable (0x80) ->
|
||||
val = 0;
|
||||
uint8_t val = 0;
|
||||
val |= (0b1) << 0; // power on
|
||||
val |= (this->is_color_enabled_() & 0b1) << 1;
|
||||
val |= (this->is_proximity_enabled_() & 0b1) << 2;
|
||||
@@ -109,7 +98,7 @@ void APDS9960::setup() {
|
||||
val |= 0b0 << 4; // color interrupt disabled
|
||||
val |= 0b0 << 5; // proximity interrupt disabled
|
||||
val |= (this->is_gesture_enabled_() & 0b1) << 6; // proximity is required for gestures
|
||||
APDS9960_WRITE_BYTE(0x80, val);
|
||||
APDS9960_ERROR_CHECK(this->write_byte(0x80, val));
|
||||
}
|
||||
bool APDS9960::is_color_enabled_() const {
|
||||
#ifdef USE_SENSOR
|
||||
|
||||
@@ -58,7 +58,7 @@ optional<ParseResult> ATCMiThermometer::parse_header_(const ble_device_base::Ser
|
||||
return {};
|
||||
}
|
||||
|
||||
auto raw = service_data.data;
|
||||
const auto &raw = service_data.data;
|
||||
if (raw.size() < 13) {
|
||||
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
|
||||
return {};
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include "bedjet_child.h"
|
||||
#include "bedjet_const.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::bedjet {
|
||||
|
||||
@@ -40,7 +42,7 @@ void BedJetHub::upgrade_firmware() {
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] MAGIC_UPDATE button failed, status=%d", this->get_name().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] MAGIC_UPDATE button failed, status=%d", this->get_name(), status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ bool BedJetHub::set_fan_index(uint8_t fan_speed_index) {
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] writing fan speed failed, status=%d", this->get_name().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] writing fan speed failed, status=%d", this->get_name(), status);
|
||||
}
|
||||
return status == 0;
|
||||
}
|
||||
@@ -82,7 +84,7 @@ bool BedJetHub::set_target_temp(float temp_c) {
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] writing target temp failed, status=%d", this->get_name().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] writing target temp failed, status=%d", this->get_name(), status);
|
||||
}
|
||||
return status == 0;
|
||||
}
|
||||
@@ -93,7 +95,7 @@ bool BedJetHub::set_time_remaining(uint8_t hours, uint8_t mins) {
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] writing remaining runtime failed, status=%d", this->get_name().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] writing remaining runtime failed, status=%d", this->get_name(), status);
|
||||
}
|
||||
return status == 0;
|
||||
}
|
||||
@@ -103,11 +105,10 @@ bool BedJetHub::send_button(BedjetButton button) {
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] writing button %s failed, status=%d", this->get_name().c_str(),
|
||||
ESP_LOGW(TAG, "[%s] writing button %s failed, status=%d", this->get_name(),
|
||||
LOG_STR_ARG(bedjet_button_to_string(button)), status);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name().c_str(),
|
||||
LOG_STR_ARG(bedjet_button_to_string(button)));
|
||||
ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name(), LOG_STR_ARG(bedjet_button_to_string(button)));
|
||||
}
|
||||
return status == 0;
|
||||
}
|
||||
@@ -125,9 +126,9 @@ uint16_t BedJetHub::get_time_remaining() {
|
||||
uint8_t BedJetHub::write_bedjet_packet_(BedjetPacket *pkt) {
|
||||
if (!this->is_connected()) {
|
||||
if (!this->parent_->enabled) {
|
||||
ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str());
|
||||
ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str());
|
||||
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name());
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -144,16 +145,16 @@ uint8_t BedJetHub::set_notify_(const bool enable) {
|
||||
status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
|
||||
this->char_handle_status_);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name(), status);
|
||||
}
|
||||
} else {
|
||||
status = esp_ble_gattc_unregister_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
|
||||
this->char_handle_status_);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name(), status);
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status);
|
||||
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name(), enable, status);
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -164,7 +165,7 @@ bool BedJetHub::discover_characteristics_() {
|
||||
if (!this->char_handle_cmd_) {
|
||||
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str());
|
||||
ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name());
|
||||
result = false;
|
||||
} else {
|
||||
this->char_handle_cmd_ = chr->handle;
|
||||
@@ -174,7 +175,7 @@ bool BedJetHub::discover_characteristics_() {
|
||||
if (!this->char_handle_status_) {
|
||||
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str());
|
||||
ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name());
|
||||
result = false;
|
||||
} else {
|
||||
this->char_handle_status_ = chr->handle;
|
||||
@@ -204,14 +205,14 @@ bool BedJetHub::discover_characteristics_() {
|
||||
if (!this->char_handle_name_) {
|
||||
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "[%s] No name service found at device, not a BedJet..?", this->get_name().c_str());
|
||||
ESP_LOGW(TAG, "[%s] No name service found at device, not a BedJet..?", this->get_name());
|
||||
result = false;
|
||||
} else {
|
||||
this->char_handle_name_ = chr->handle;
|
||||
auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
|
||||
this->char_handle_name_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status);
|
||||
ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name(), status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,7 +223,7 @@ bool BedJetHub::discover_characteristics_() {
|
||||
" - Status char: 0x%x\n"
|
||||
" - config descriptor: 0x%x\n"
|
||||
" - Name char: 0x%x",
|
||||
this->get_name().c_str(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_,
|
||||
this->get_name(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_,
|
||||
this->char_handle_name_);
|
||||
|
||||
return result;
|
||||
@@ -241,7 +242,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
auto result = this->discover_characteristics_();
|
||||
|
||||
if (result) {
|
||||
ESP_LOGD(TAG, "[%s] Services complete: obtained char handles.", this->get_name().c_str());
|
||||
ESP_LOGD(TAG, "[%s] Services complete: obtained char handles.", this->get_name());
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
this->set_notify_(true);
|
||||
|
||||
@@ -253,7 +254,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
|
||||
this->dispatch_state_(true);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[%s] Failed discovering service characteristics.", this->get_name().c_str());
|
||||
ESP_LOGW(TAG, "[%s] Failed discovering service characteristics.", this->get_name());
|
||||
this->parent()->set_enabled(false);
|
||||
this->status_set_warning();
|
||||
this->dispatch_state_(false);
|
||||
@@ -265,15 +266,15 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
if (param->write.status == ESP_GATT_INVALID_ATTR_LEN) {
|
||||
// This probably means that our hack for notify_en (8 bit vs 16 bit) didn't work right.
|
||||
// Should we try to fall back to BLEClient's way?
|
||||
ESP_LOGW(TAG, "[%s] Invalid attr length writing descr at handle 0x%04d, status=%d", this->get_name().c_str(),
|
||||
ESP_LOGW(TAG, "[%s] Invalid attr length writing descr at handle 0x%04d, status=%d", this->get_name(),
|
||||
param->write.handle, param->write.status);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name().c_str(),
|
||||
param->write.handle, param->write.status);
|
||||
ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name(), param->write.handle,
|
||||
param->write.status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle,
|
||||
ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name(), param->write.handle,
|
||||
param->write.status);
|
||||
break;
|
||||
}
|
||||
@@ -306,9 +307,11 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
} else if (param->read.handle == this->char_handle_name_) {
|
||||
// The data should represent the name.
|
||||
if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) {
|
||||
std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len);
|
||||
ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str());
|
||||
this->set_name_(bedjet_name);
|
||||
ESP_LOGV(TAG, "[%s] Got BedJet name: '%.*s'", this->get_name(), static_cast<int>(param->read.value_len),
|
||||
reinterpret_cast<const char *>(param->read.value));
|
||||
const size_t len = std::min<size_t>(param->read.value_len, sizeof(this->name_) - 1);
|
||||
memcpy(this->name_, param->read.value, len);
|
||||
this->name_[len] = '\0';
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -322,8 +325,8 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
// doesn't break anything.
|
||||
|
||||
if (param->reg_for_notify.handle != this->char_handle_status_) {
|
||||
ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x",
|
||||
this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_);
|
||||
ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", this->get_name(),
|
||||
param->reg_for_notify.handle, this->char_handle_status_);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -335,8 +338,8 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
|
||||
// This event is not handled by the parent BLEClient, so we need to do this either way.
|
||||
if (param->unreg_for_notify.handle != this->char_handle_status_) {
|
||||
ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x",
|
||||
this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_);
|
||||
ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", this->get_name(),
|
||||
param->unreg_for_notify.handle, this->char_handle_status_);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -350,13 +353,13 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
break;
|
||||
|
||||
if (param->notify.conn_id != this->parent_->get_conn_id()) {
|
||||
ESP_LOGW(TAG, "[%s] Received notify event for unexpected parent conn: expect %x, got %x",
|
||||
this->get_name().c_str(), this->parent_->get_conn_id(), param->notify.conn_id);
|
||||
ESP_LOGW(TAG, "[%s] Received notify event for unexpected parent conn: expect %x, got %x", this->get_name(),
|
||||
this->parent_->get_conn_id(), param->notify.conn_id);
|
||||
// FIXME: bug in BLEClient holding wrong conn_id.
|
||||
}
|
||||
|
||||
if (param->notify.handle != this->char_handle_status_) {
|
||||
ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(),
|
||||
ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name(),
|
||||
this->char_handle_status_, param->notify.handle);
|
||||
break;
|
||||
}
|
||||
@@ -372,13 +375,13 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
if (!this->force_refresh_ && this->codec_->compare(param->notify.value, param->notify.value_len)) {
|
||||
// If the packet is meaningfully different, trigger children as well
|
||||
this->force_refresh_ = true;
|
||||
ESP_LOGV(TAG, "[%s] Incoming packet indicates a significant change.", this->get_name().c_str());
|
||||
ESP_LOGV(TAG, "[%s] Incoming packet indicates a significant change.", this->get_name());
|
||||
}
|
||||
|
||||
if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) {
|
||||
// Set reentrant flag to prevent processing multiple packets.
|
||||
this->processing_ = true;
|
||||
ESP_LOGVV(TAG, "[%s] Decoding packet: last=%" PRId32 ", delta=%" PRId32 ", force=%s", this->get_name().c_str(),
|
||||
ESP_LOGVV(TAG, "[%s] Decoding packet: last=%" PRId32 ", delta=%" PRId32 ", force=%s", this->get_name(),
|
||||
this->last_notify_, delta, this->force_refresh_ ? "y" : "n");
|
||||
bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len);
|
||||
|
||||
@@ -388,7 +391,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
|
||||
this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str());
|
||||
ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name());
|
||||
}
|
||||
} else {
|
||||
this->status_packet_ready_();
|
||||
@@ -397,7 +400,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event);
|
||||
ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name(), event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -436,7 +439,7 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
|
||||
return status;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name().c_str(),
|
||||
ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name(),
|
||||
enable ? "true" : "false", handle, this->parent_->get_conn_id());
|
||||
return ESP_GATT_OK;
|
||||
}
|
||||
@@ -468,7 +471,7 @@ void BedJetHub::setup_time_() {
|
||||
|
||||
void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
|
||||
if (!this->is_connected()) {
|
||||
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
|
||||
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -477,7 +480,7 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
|
||||
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name(), hour, minute);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,7 +498,7 @@ void BedJetHub::dump_config() {
|
||||
"BedJet Hub '%s'\n"
|
||||
" ble_client.app_id: %d\n"
|
||||
" ble_client.conn_id: %d",
|
||||
this->get_name().c_str(), this->parent()->app_id, this->parent()->get_conn_id());
|
||||
this->get_name(), this->parent()->app_id, this->parent()->get_conn_id());
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
ESP_LOGCONFIG(TAG, " Child components (%d):", this->children_.size());
|
||||
for (auto *child : this->children_) {
|
||||
@@ -513,10 +516,9 @@ void BedJetHub::dispatch_status_() {
|
||||
auto *status = this->codec_->get_status_packet();
|
||||
|
||||
if (!this->is_connected()) {
|
||||
ESP_LOGD(TAG, "[%s] Not connected, will not send status.", this->get_name().c_str());
|
||||
ESP_LOGD(TAG, "[%s] Not connected, will not send status.", this->get_name());
|
||||
} else if (status != nullptr) {
|
||||
ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name().c_str(), this->children_.size(),
|
||||
status);
|
||||
ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name(), this->children_.size(), status);
|
||||
for (auto *child : this->children_) {
|
||||
child->on_status(status);
|
||||
}
|
||||
@@ -530,13 +532,13 @@ void BedJetHub::dispatch_status_() {
|
||||
// We can try to unregister for notifications now, and then re-register, hoping to clear it up...
|
||||
// But how do we know for sure which state we're in, and how do we actually clear out the buggy state?
|
||||
|
||||
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str());
|
||||
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name());
|
||||
} else if (diff > NOTIFY_WARN_THRESHOLD) {
|
||||
ESP_LOGW(TAG, "[%s] Last GATT notify was %" PRId32 " seconds ago.", this->get_name().c_str(), diff / 1000);
|
||||
ESP_LOGW(TAG, "[%s] Last GATT notify was %" PRId32 " seconds ago.", this->get_name(), diff / 1000);
|
||||
}
|
||||
|
||||
if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) {
|
||||
ESP_LOGW(TAG, "[%s] Timed out after %" PRId32 " sec. Retrying", this->get_name().c_str(), this->timeout_);
|
||||
ESP_LOGW(TAG, "[%s] Timed out after %" PRId32 " sec. Retrying", this->get_name(), this->timeout_);
|
||||
// set_enabled(false) will only close the connection if state != IDLE.
|
||||
this->parent()->set_state(espbt::ClientState::CONNECTING);
|
||||
this->parent()->set_enabled(false);
|
||||
|
||||
@@ -119,13 +119,7 @@ class BedJetHub final : public esphome::ble_client::BLEClientNode, public Pollin
|
||||
float get_setup_priority() const override { return setup_priority::BLUETOOTH; }
|
||||
|
||||
/** @return The BedJet's configured name, or the MAC address if not discovered yet. */
|
||||
std::string get_name() {
|
||||
if (this->name_.empty()) {
|
||||
return this->parent_->address_str();
|
||||
} else {
|
||||
return this->name_;
|
||||
}
|
||||
}
|
||||
const char *get_name() const { return this->name_[0] == '\0' ? this->parent_->address_str() : this->name_; }
|
||||
|
||||
/* BLEClient overrides */
|
||||
|
||||
@@ -151,9 +145,8 @@ class BedJetHub final : public esphome::ble_client::BLEClientNode, public Pollin
|
||||
uint8_t set_notify_(bool enable);
|
||||
/** Send the `BedjetPacket` to the device. */
|
||||
uint8_t write_bedjet_packet_(BedjetPacket *pkt);
|
||||
void set_name_(const std::string &name) { this->name_ = name; }
|
||||
|
||||
std::string name_;
|
||||
// Only used in log lines; longer names are truncated
|
||||
char name_[24]{};
|
||||
|
||||
uint32_t last_notify_ = 0;
|
||||
inline void status_packet_ready_();
|
||||
|
||||
@@ -62,6 +62,13 @@ struct SpiData {
|
||||
|
||||
static SpiData *spi_data = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// BDK 3.0.78 makes REG_WRITE a statement expression that yields the volatile assignment,
|
||||
// a deprecated volatile read under clang
|
||||
static inline void reg_write(uint32_t addr, uint32_t value) {
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr) - MMIO register at a fixed address
|
||||
*reinterpret_cast<volatile uint32_t *>(addr) = value;
|
||||
}
|
||||
|
||||
static void set_spi_ctrl_register(uint32_t bit, bool val) {
|
||||
uint32_t value = REG_READ(SPI_CTRL);
|
||||
if (val == 0) {
|
||||
@@ -69,7 +76,7 @@ static void set_spi_ctrl_register(uint32_t bit, bool val) {
|
||||
} else if (val == 1) {
|
||||
value |= bit;
|
||||
}
|
||||
REG_WRITE(SPI_CTRL, value);
|
||||
reg_write(SPI_CTRL, value);
|
||||
}
|
||||
|
||||
#ifdef BEKEN_SPI_IS_N_LIKE
|
||||
@@ -80,7 +87,7 @@ static void set_spi_config_register(uint32_t bit, bool val) {
|
||||
} else if (val == 1) {
|
||||
value |= bit;
|
||||
}
|
||||
REG_WRITE(SPI_CONFIG, value);
|
||||
reg_write(SPI_CONFIG, value);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -136,7 +143,7 @@ static void spi_set_clock(uint32_t max_hz) {
|
||||
param = REG_READ(SPI_CTRL);
|
||||
param &= ~(SPI_CKR_MASK << SPI_CKR_POSI);
|
||||
param |= (div << SPI_CKR_POSI);
|
||||
REG_WRITE(SPI_CTRL, param);
|
||||
reg_write(SPI_CTRL, param);
|
||||
#ifdef BEKEN_SPI_IS_N_LIKE
|
||||
const int divisor = div;
|
||||
#else
|
||||
@@ -229,7 +236,7 @@ void BekenSPILEDStripLightOutput::setup() {
|
||||
value = REG_READ(SPI_CTRL);
|
||||
value &= ~CTRL_NSSMD_3;
|
||||
value |= (1 << 17);
|
||||
REG_WRITE(SPI_CTRL, value);
|
||||
reg_write(SPI_CTRL, value);
|
||||
|
||||
#ifdef BEKEN_SPI_IS_N_LIKE
|
||||
value = GFUNC_MODE_SPI_DMA;
|
||||
@@ -279,7 +286,7 @@ void BekenSPILEDStripLightOutput::setup() {
|
||||
value = REG_READ(SPI_CONFIG);
|
||||
value &= ~(0xFFF << 8);
|
||||
value |= ((dma_buffer_size & 0xFFF) << 8);
|
||||
REG_WRITE(SPI_CONFIG, value);
|
||||
reg_write(SPI_CONFIG, value);
|
||||
#else
|
||||
// T-like variants: No transfer-length register. The SPI controller shifts out
|
||||
// everything the DMA writes into SPI_DAT.
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_BH1745_ID, BH1745Component, bh1745_ns
|
||||
from .. import CONF_BH1745_ID, BH1745Component
|
||||
|
||||
DEPENDENCIES = ["bh1745"]
|
||||
|
||||
BH1745InterruptPinOutput = bh1745_ns.class_(
|
||||
"BH1745InterruptPinOutput", output.BinaryOutput
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BH1745InterruptPinOutput),
|
||||
cv.GenerateID(): cv.declare_id(output.ApplyBinaryOutput),
|
||||
cv.GenerateID(CONF_BH1745_ID): cv.use_id(BH1745Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await output.register_output(var, config)
|
||||
hub = await cg.get_variable(config[CONF_BH1745_ID])
|
||||
await cg.register_parented(var, hub)
|
||||
await output.sub_binary_outputs(config, parent=hub).create(
|
||||
config, None, apply="set_interrupt_state(state)"
|
||||
)
|
||||
cg.add(hub.enable_interrupt_output())
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/output/binary_output.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "../bh1745.h"
|
||||
|
||||
namespace esphome::bh1745 {
|
||||
|
||||
/// Drives the BH1745 INT pin, which switches the LEDs on breakout boards such as the Pimoroni one.
|
||||
class BH1745InterruptPinOutput : public output::BinaryOutput, public Parented<BH1745Component> {
|
||||
protected:
|
||||
void write_state(bool state) override { this->parent_->set_interrupt_state(state); }
|
||||
};
|
||||
|
||||
} // namespace esphome::bh1745
|
||||
@@ -21,9 +21,10 @@ public ble_api.h.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import libretiny, wifi
|
||||
from esphome.components import libretiny
|
||||
from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7231Q,
|
||||
@@ -32,14 +33,30 @@ from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7251,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.const import (
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_ESPHOME,
|
||||
CONF_ID,
|
||||
CONF_PLATFORMIO_OPTIONS,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["bk72xx"]
|
||||
CODEOWNERS = ["@Bl00d-B0b"]
|
||||
DOMAIN = "bk72xx_ble"
|
||||
|
||||
BEKEN_BDK_VERSION_OPTION = "custom_versions.beken-bdk"
|
||||
# Older BDKs lay the BLE stack out differently and can crash at boot or erase the
|
||||
# bootloader with BLE compiled in; the libretiny platform builds with this one
|
||||
MIN_BDK_VERSION = (3, 0, 78)
|
||||
# Older LibreTiny releases arm the Beken MCU sleep with wifi power save, which halts
|
||||
# the device once the station stops while BLE runs (libretiny-eu/libretiny#414)
|
||||
MIN_LIBRETINY_VERSION = (1, 14, 0)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
bk72xx_ble_ns = cg.esphome_ns.namespace("bk72xx_ble")
|
||||
@@ -80,20 +97,45 @@ def _unsupported_family_message(family: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def check_bdk_version(value: str | None) -> None:
|
||||
"""Reject a beken-bdk override older than the release the BLE stack needs."""
|
||||
if value is None:
|
||||
return
|
||||
# LibreTiny appends the value to the package URL as a git ref and only builds
|
||||
# 3.0.x releases; the 2021.06.07 tag is the pre 3.0 SDK
|
||||
match = re.fullmatch(r"v?(\d+)\.(\d+)\.(\d+)", str(value))
|
||||
if match is not None:
|
||||
version = tuple(int(g) for g in match.groups())
|
||||
if version[:2] == MIN_BDK_VERSION[:2] and version >= MIN_BDK_VERSION:
|
||||
return
|
||||
minimum = ".".join(str(part) for part in MIN_BDK_VERSION)
|
||||
raise cv.Invalid(
|
||||
f"bk72xx_ble needs beken-bdk {minimum} or a newer 3.0.x release, not "
|
||||
f"'{value}': with BLE compiled in, an older BDK can crash at boot or erase "
|
||||
"the bootloader"
|
||||
)
|
||||
|
||||
|
||||
def check_libretiny_version(version: cv.Version) -> None:
|
||||
"""Reject a LibreTiny release older than the one whose WiFi sleep is safe with BLE."""
|
||||
if (version.major, version.minor, version.patch) >= MIN_LIBRETINY_VERSION:
|
||||
return
|
||||
minimum = ".".join(str(part) for part in MIN_LIBRETINY_VERSION)
|
||||
raise cv.Invalid(
|
||||
f"bk72xx_ble needs LibreTiny {minimum} or newer, not {version}: older releases "
|
||||
"arm the Beken MCU sleep with wifi power save and the device halts once the "
|
||||
"station stops while BLE runs"
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
# Warn only: a hard error here would break the validate-only CI fixtures,
|
||||
# which run on a BLE 4.2 board. The hard error is raised at codegen.
|
||||
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
|
||||
_LOGGER.warning("%s (this configuration cannot compile)", msg)
|
||||
# Any wifi power_save_mode other than NONE also arms the Beken SDK's MCU
|
||||
# sleep. With the BLE controller running, that sleep never wakes up once the
|
||||
# station is stopped (adapter restart after failed roams, wifi.disable): the
|
||||
# device is dead until a power cycle (esphome#18592). Keep power save off
|
||||
# until LibreTiny ships the SDK-side fix (libretiny-eu/libretiny#414).
|
||||
wifi.force_power_save_off(
|
||||
"with BLE running, the Beken SDK's MCU sleep halts the device once the "
|
||||
"station is stopped (https://github.com/esphome/esphome/issues/18592)"
|
||||
)
|
||||
options = fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS) or {}
|
||||
check_bdk_version(options.get(BEKEN_BDK_VERSION_OPTION))
|
||||
check_libretiny_version(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION])
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
@@ -116,20 +158,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
# so this =1 is the single CFG_SUPPORT_BLE define emitted.
|
||||
cg.add_platformio_option("custom_options.sys_config#h", ["CFG_SUPPORT_BLE=1"])
|
||||
|
||||
# Pin the Beken BDK release the BLE 5.x stack is validated against. The
|
||||
# bundled 3.0.33 has an older BLE header/library layout — and with
|
||||
# CFG_SUPPORT_BLE=1 the SDK runs its BLE init unconditionally during boot
|
||||
# (the reason the libretiny platform sets =0 when BLE is unused), so a
|
||||
# mismatched BDK can crash the device before WiFi comes up regardless of
|
||||
# enable_on_boot. Pinning here makes a plain config build against the
|
||||
# validated BDK without any manual platformio_options.
|
||||
_LOGGER.warning(
|
||||
"bk72xx_ble builds with beken-bdk 3.0.78 instead of the platform's bundled "
|
||||
"default: the default's older BLE layout can crash the device at boot when "
|
||||
"BLE is compiled in"
|
||||
)
|
||||
cg.add_platformio_option("custom_versions.beken-bdk", "3.0.78")
|
||||
|
||||
# The BDK exposes the controller's BLE address as `common_default_bdaddr` on
|
||||
# BK7231N, but NOT on BK7238 (its BLE stack has no such symbol; the address is
|
||||
# derived from the WiFi MAC instead — the BDK's own fallback). Tell the C++
|
||||
|
||||
@@ -80,7 +80,8 @@ void BP1658CJ::loop() {
|
||||
this->update_ = false;
|
||||
}
|
||||
|
||||
void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) {
|
||||
void BP1658CJ::write_channel(uint8_t channel, float state) {
|
||||
auto value = static_cast<uint16_t>(state * 0x3FF);
|
||||
if (this->pwm_amounts_[channel] != value) {
|
||||
this->update_ = true;
|
||||
this->update_channel_ = channel;
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::bp1658cj {
|
||||
|
||||
class BP1658CJ final : public Component {
|
||||
public:
|
||||
class Channel;
|
||||
|
||||
void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
|
||||
void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
|
||||
void set_max_power_color_channels(uint8_t max_power_color_channels) {
|
||||
@@ -29,23 +26,9 @@ class BP1658CJ final : public Component {
|
||||
/// Send new values if they were updated.
|
||||
void loop() override;
|
||||
|
||||
class Channel final : public output::FloatOutput {
|
||||
public:
|
||||
void set_parent(BP1658CJ *parent) { parent_ = parent; }
|
||||
void set_channel(uint8_t channel) { channel_ = channel; }
|
||||
|
||||
protected:
|
||||
void write_state(float state) override {
|
||||
auto amount = static_cast<uint16_t>(state * 0x3FF);
|
||||
this->parent_->set_channel_value_(this->channel_, amount);
|
||||
}
|
||||
|
||||
BP1658CJ *parent_;
|
||||
uint8_t channel_;
|
||||
};
|
||||
void write_channel(uint8_t channel, float state);
|
||||
|
||||
protected:
|
||||
void set_channel_value_(uint8_t channel, uint16_t value);
|
||||
void write_bit_(bool value);
|
||||
void write_byte_(uint8_t data);
|
||||
void write_buffer_(uint8_t *buffer, uint8_t size);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_ID
|
||||
@@ -8,16 +7,15 @@ from . import BP1658CJ
|
||||
|
||||
DEPENDENCIES = ["bp1658cj"]
|
||||
|
||||
Channel = BP1658CJ.class_("Channel", output.FloatOutput)
|
||||
|
||||
CONF_BP1658CJ_ID = "bp1658cj_id"
|
||||
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_BP1658CJ_ID): cv.use_id(BP1658CJ),
|
||||
cv.Required(CONF_ID): cv.declare_id(Channel),
|
||||
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
|
||||
@@ -26,8 +24,6 @@ FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await output.register_output(var, config)
|
||||
|
||||
await cg.register_parented(var, config[CONF_BP1658CJ_ID])
|
||||
cg.add(var.set_channel(config[CONF_CHANNEL]))
|
||||
await output.sub_float_outputs(config, parent=config[CONF_BP1658CJ_ID]).create(
|
||||
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
|
||||
)
|
||||
@@ -14,11 +14,7 @@ void BQ25186TextSensor::on_status(const BQ25186StatusRegisters &status) {
|
||||
if (value == this->last_)
|
||||
return;
|
||||
this->last_ = value;
|
||||
const ProgmemStr name = StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX);
|
||||
char text[32];
|
||||
ESPHOME_strncpy_P(text, reinterpret_cast<ESPHOME_PGM_P>(name), sizeof(text));
|
||||
text[sizeof(text) - 1] = '\0';
|
||||
this->publish_state(text);
|
||||
this->publish_state(StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX));
|
||||
}
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
@@ -163,26 +163,21 @@ void CW2017::update() {
|
||||
if (!this->ready_)
|
||||
return;
|
||||
|
||||
bool ok = true;
|
||||
if (this->battery_level_sensor_ != nullptr) {
|
||||
uint8_t soc = 0;
|
||||
if (this->read_byte(REG_SOC, &soc) && soc <= 100) {
|
||||
ok = this->read_byte(REG_SOC, &soc) && soc <= 100;
|
||||
if (ok)
|
||||
this->battery_level_sensor_->publish_state(soc);
|
||||
} else {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->battery_voltage_sensor_ != nullptr) {
|
||||
if (ok && this->battery_voltage_sensor_ != nullptr) {
|
||||
uint16_t raw = 0;
|
||||
if (this->read_byte_16(REG_VCELL, &raw)) {
|
||||
ok = this->read_byte_16(REG_VCELL, &raw);
|
||||
if (ok)
|
||||
this->battery_voltage_sensor_->publish_state((raw & VCELL_MASK) * VCELL_VOLTS_PER_LSB);
|
||||
} else {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->status_clear_warning();
|
||||
this->status_comm_result(ok);
|
||||
}
|
||||
|
||||
void CW2017::dump_config() {
|
||||
|
||||
@@ -12,8 +12,8 @@ class CW2017 : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
explicit CW2017(const uint8_t *profile) : profile_(profile) {}
|
||||
|
||||
void set_battery_level_sensor(sensor::Sensor *sensor) { this->battery_level_sensor_ = sensor; }
|
||||
void set_battery_voltage_sensor(sensor::Sensor *sensor) { this->battery_voltage_sensor_ = sensor; }
|
||||
SUB_SENSOR(battery_level)
|
||||
SUB_SENSOR(battery_voltage)
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
@@ -29,8 +29,6 @@ class CW2017 : public PollingComponent, public i2c::I2CDevice {
|
||||
void wait_ready_(uint8_t attempt);
|
||||
|
||||
const uint8_t *profile_;
|
||||
sensor::Sensor *battery_level_sensor_{nullptr};
|
||||
sensor::Sensor *battery_voltage_sensor_{nullptr};
|
||||
bool ready_{false};
|
||||
bool restart_pending_{false};
|
||||
uint8_t init_attempts_{0};
|
||||
|
||||
@@ -48,15 +48,14 @@ void DAC7678Output::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
void DAC7678Output::register_channel(DAC7678Channel *channel) {
|
||||
auto c = channel->channel_;
|
||||
this->min_channel_ = std::min(this->min_channel_, c);
|
||||
this->max_channel_ = std::max(this->max_channel_, c);
|
||||
channel->set_parent(this);
|
||||
ESP_LOGV(TAG, "Registered channel: %01u", channel->channel_);
|
||||
void DAC7678Output::register_channel(uint8_t channel) {
|
||||
this->min_channel_ = std::min(this->min_channel_, channel);
|
||||
this->max_channel_ = std::max(this->max_channel_, channel);
|
||||
ESP_LOGV(TAG, "Registered channel: %01u", channel);
|
||||
}
|
||||
|
||||
void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) {
|
||||
void DAC7678Output::write_channel(uint8_t channel, float state) {
|
||||
auto value = static_cast<uint16_t>(roundf(state * 0xFFF));
|
||||
if (channel >= std::size(this->dac_input_reg_))
|
||||
return;
|
||||
if (this->dac_input_reg_[channel] != value) {
|
||||
@@ -71,10 +70,4 @@ void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void DAC7678Channel::write_state(float state) {
|
||||
const float input_rounded = roundf(state * this->full_scale_);
|
||||
auto input = static_cast<uint16_t>(input_rounded);
|
||||
this->parent_->set_channel_value_(this->channel_, input);
|
||||
}
|
||||
|
||||
} // namespace esphome::dac7678
|
||||
@@ -1,34 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::dac7678 {
|
||||
|
||||
class DAC7678Output;
|
||||
|
||||
class DAC7678Channel final : public output::FloatOutput, public Parented<DAC7678Output> {
|
||||
public:
|
||||
void set_channel(uint8_t channel) { channel_ = channel; }
|
||||
|
||||
protected:
|
||||
friend class DAC7678Output;
|
||||
|
||||
const uint16_t full_scale_ = 0xFFF;
|
||||
|
||||
void write_state(float state) override;
|
||||
|
||||
uint8_t channel_;
|
||||
};
|
||||
|
||||
/// DAC7678 float output component.
|
||||
class DAC7678Output final : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
DAC7678Output() {}
|
||||
|
||||
void register_channel(DAC7678Channel *channel);
|
||||
void register_channel(uint8_t channel);
|
||||
void write_channel(uint8_t channel, float state);
|
||||
|
||||
void set_internal_reference(const bool value) { this->internal_reference_ = value; }
|
||||
|
||||
@@ -37,12 +20,8 @@ class DAC7678Output final : public Component, public i2c::I2CDevice {
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
protected:
|
||||
friend DAC7678Channel;
|
||||
|
||||
bool internal_reference_;
|
||||
|
||||
void set_channel_value_(uint8_t channel, uint16_t value);
|
||||
|
||||
uint8_t min_channel_{0xFF};
|
||||
uint8_t max_channel_{0x00};
|
||||
uint16_t dac_input_reg_[8] = {
|
||||
|
||||
@@ -2,19 +2,17 @@ import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import DAC7678Output, dac7678_ns
|
||||
from . import DAC7678Output
|
||||
|
||||
DEPENDENCIES = ["dac7678"]
|
||||
|
||||
DAC7678Channel = dac7678_ns.class_("DAC7678Channel", output.FloatOutput)
|
||||
CONF_DAC7678_ID = "dac7678_id"
|
||||
|
||||
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(DAC7678Channel),
|
||||
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
|
||||
cv.GenerateID(CONF_DAC7678_ID): cv.use_id(DAC7678Output),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
|
||||
}
|
||||
@@ -26,10 +24,9 @@ FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_DAC7678_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_channel(config[CONF_CHANNEL]))
|
||||
cg.add(paren.register_channel(var))
|
||||
await output.register_output(var, config)
|
||||
return var
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DAC7678_ID])
|
||||
await output.sub_float_outputs(config, parent=hub).create(
|
||||
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
|
||||
)
|
||||
cg.add(hub.register_channel(config[CONF_CHANNEL]))
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
@@ -27,9 +29,12 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
|
||||
TEMPLATABLE_VALUE(float, det_max4)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(0));
|
||||
// Worst case: stop, factory reset, six settings, save, start
|
||||
if (!this->parent_->has_queue_room(10))
|
||||
return;
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, false);
|
||||
if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) {
|
||||
this->parent_->enqueue(make_unique<FactoryResetCommand>());
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_FACTORY_RESET);
|
||||
}
|
||||
if (this->det_min1_.has_value() && this->det_max1_.has_value()) {
|
||||
// Each segment is read once, so the guard and the command see the same values.
|
||||
@@ -37,10 +42,10 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
|
||||
if (min1 >= 0) {
|
||||
const float max1 = this->det_max1_.value(x...);
|
||||
if (max1 >= 0) {
|
||||
this->parent_->enqueue(make_unique<DetRangeCfgCommand>(
|
||||
min1, max1, this->det_min2_.value_or(x..., -1), this->det_max2_.value_or(x..., -1),
|
||||
this->det_min3_.value_or(x..., -1), this->det_max3_.value_or(x..., -1),
|
||||
this->det_min4_.value_or(x..., -1), this->det_max4_.value_or(x..., -1)));
|
||||
this->parent_->enqueue_det_range_cfg(min1, max1, this->det_min2_.value_or(x..., -1),
|
||||
this->det_max2_.value_or(x..., -1), this->det_min3_.value_or(x..., -1),
|
||||
this->det_max3_.value_or(x..., -1), this->det_min4_.value_or(x..., -1),
|
||||
this->det_max4_.value_or(x..., -1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,38 +53,35 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
|
||||
float detect = this->delay_after_detect_.value(x...);
|
||||
float disappear = this->delay_after_disappear_.value(x...);
|
||||
if (detect >= 0 && disappear >= 0) {
|
||||
this->parent_->enqueue(make_unique<SetLatencyCommand>(detect, disappear));
|
||||
this->parent_->enqueue_set_latency(detect, disappear);
|
||||
}
|
||||
}
|
||||
if (this->start_after_power_on_.has_value()) {
|
||||
int8_t val = this->start_after_power_on_.value(x...);
|
||||
if (val >= 0) {
|
||||
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(val));
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_SENSOR_CFG_START, val);
|
||||
}
|
||||
}
|
||||
if (this->turn_on_led_.has_value()) {
|
||||
int8_t val = this->turn_on_led_.value(x...);
|
||||
if (val >= 0) {
|
||||
this->parent_->enqueue(make_unique<LedModeCommand>(val));
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_LED_MODE, val);
|
||||
}
|
||||
}
|
||||
if (this->presence_via_uart_.has_value()) {
|
||||
int8_t val = this->presence_via_uart_.value(x...);
|
||||
if (val >= 0) {
|
||||
this->parent_->enqueue(make_unique<UartOutputCommand>(val));
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_UART_OUTPUT, val);
|
||||
}
|
||||
}
|
||||
if (this->sensitivity_.has_value()) {
|
||||
int8_t val = this->sensitivity_.value(x...);
|
||||
if (val >= 0) {
|
||||
if (val > 9) {
|
||||
val = 9;
|
||||
}
|
||||
this->parent_->enqueue(make_unique<SensitivityCommand>(val));
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_SENSITIVITY, std::min<int8_t>(val, 9));
|
||||
}
|
||||
}
|
||||
this->parent_->enqueue(make_unique<SaveCfgCommand>());
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(1));
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_SAVE_CFG);
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
#include "commands.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "dfrobot_sen0395.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395.commands");
|
||||
|
||||
uint8_t Command::execute(DfrobotSen0395Component *parent) {
|
||||
this->parent_ = parent;
|
||||
if (this->cmd_sent_) {
|
||||
if (this->parent_->read_message_()) {
|
||||
std::string message(this->parent_->read_buffer_);
|
||||
if (message.rfind("is not recognized as a CLI command") != std::string::npos) {
|
||||
ESP_LOGD(TAG, "Command not recognized properly by sensor");
|
||||
if (this->retries_left_ > 0) {
|
||||
this->retries_left_ -= 1;
|
||||
this->cmd_sent_ = false;
|
||||
ESP_LOGD(TAG, "Retrying");
|
||||
return 0;
|
||||
} else {
|
||||
this->parent_->find_prompt_();
|
||||
return 1; // Command done
|
||||
}
|
||||
}
|
||||
uint8_t rc = on_message(message);
|
||||
if (rc == 2) {
|
||||
if (this->retries_left_ > 0) {
|
||||
this->retries_left_ -= 1;
|
||||
this->cmd_sent_ = false;
|
||||
ESP_LOGD(TAG, "Retrying");
|
||||
return 0;
|
||||
} else {
|
||||
this->parent_->find_prompt_();
|
||||
return 1; // Command done
|
||||
}
|
||||
} else if (rc == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
this->parent_->find_prompt_();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) {
|
||||
ESP_LOGD(TAG, "Command timeout");
|
||||
if (this->retries_left_ > 0) {
|
||||
this->retries_left_ -= 1;
|
||||
this->cmd_sent_ = false;
|
||||
ESP_LOGD(TAG, "Retrying");
|
||||
} else {
|
||||
return 1; // Command done
|
||||
}
|
||||
}
|
||||
} else if (this->parent_->send_cmd_(this->cmd_.c_str(), this->cmd_duration_ms_)) {
|
||||
this->cmd_sent_ = true;
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t ReadStateCommand::execute(DfrobotSen0395Component *parent) {
|
||||
this->parent_ = parent;
|
||||
if (this->parent_->read_message_()) {
|
||||
std::string message(this->parent_->read_buffer_);
|
||||
if (message.rfind("$JYBSS,0, , , *") != std::string::npos) {
|
||||
this->parent_->set_detected_(false);
|
||||
this->parent_->set_active(true);
|
||||
return 1; // Command done
|
||||
} else if (message.rfind("$JYBSS,1, , , *") != std::string::npos) {
|
||||
this->parent_->set_detected_(true);
|
||||
this->parent_->set_active(true);
|
||||
return 1; // Command done
|
||||
}
|
||||
}
|
||||
if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) {
|
||||
return 1; // Command done, timeout
|
||||
}
|
||||
return 0; // Command not done yet.
|
||||
}
|
||||
|
||||
uint8_t ReadStateCommand::on_message(std::string &message) { return 1; }
|
||||
|
||||
uint8_t PowerCommand::on_message(std::string &message) {
|
||||
if (message == "sensor stopped already") {
|
||||
this->parent_->set_active(false);
|
||||
ESP_LOGI(TAG, "Stopped sensor (already stopped)");
|
||||
return 1; // Command done
|
||||
} else if (message == "sensor started already") {
|
||||
this->parent_->set_active(true);
|
||||
ESP_LOGI(TAG, "Started sensor (already started)");
|
||||
return 1; // Command done
|
||||
} else if (message == "new parameter isn't save, can't startSensor") {
|
||||
this->parent_->set_active(false);
|
||||
ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
this->parent_->set_active(this->power_on_);
|
||||
if (this->power_on_) {
|
||||
ESP_LOGI(TAG, "Started sensor");
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Stopped sensor");
|
||||
}
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet.
|
||||
}
|
||||
|
||||
DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3,
|
||||
float min4, float max4) {
|
||||
// TODO: Print warning when values are rounded
|
||||
if (min1 < 0 || max1 < 0) {
|
||||
this->min1_ = min1 = 0;
|
||||
this->max1_ = max1 = 0;
|
||||
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
|
||||
this->max4_ = max4 = -1;
|
||||
|
||||
ESP_LOGW(TAG, "DetRangeCfgCommand invalid input parameters. Using range config 0 0.");
|
||||
|
||||
this->cmd_ = "detRangeCfg -1 0 0";
|
||||
} else if (min2 < 0 || max2 < 0) {
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
|
||||
this->max4_ = max4 = -1;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15f, max1 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
} else if (min3 < 0 || max3 < 0) {
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
|
||||
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
|
||||
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
|
||||
max2 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
} else if (min4 < 0 || max4 < 0) {
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
|
||||
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
|
||||
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
|
||||
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
|
||||
this->min4_ = min4 = this->max4_ = max4 = -1;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
|
||||
max2 / 0.15f, min3 / 0.15f, max3 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
} else {
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
|
||||
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
|
||||
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
|
||||
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
|
||||
this->min4_ = min4 = roundf(min4 / 0.15f) * 0.15f;
|
||||
this->max4_ = max4 = roundf(max4 / 0.15f) * 0.15f;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f,
|
||||
min2 / 0.15f, max2 / 0.15f, min3 / 0.15f, max3 / 0.15f, min4 / 0.15f, max4 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
}
|
||||
|
||||
this->min1_ = min1;
|
||||
this->max1_ = max1;
|
||||
this->min2_ = min2;
|
||||
this->max2_ = max2;
|
||||
this->min3_ = min3;
|
||||
this->max3_ = max3;
|
||||
this->min4_ = min4;
|
||||
this->max4_ = max4;
|
||||
};
|
||||
|
||||
uint8_t DetRangeCfgCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot configure range config. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG,
|
||||
"Updated detection area config:\n"
|
||||
" Detection area 1 from %.02fm to %.02fm.",
|
||||
this->min1_, this->max1_);
|
||||
if (this->min2_ >= 0 && this->max2_ >= 0) {
|
||||
ESP_LOGI(TAG, " Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_);
|
||||
}
|
||||
if (this->min3_ >= 0 && this->max3_ >= 0) {
|
||||
ESP_LOGI(TAG, " Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_);
|
||||
}
|
||||
if (this->min4_ >= 0 && this->max4_ >= 0) {
|
||||
ESP_LOGI(TAG, " Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_);
|
||||
}
|
||||
ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet.
|
||||
}
|
||||
|
||||
SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_after_disappear) {
|
||||
delay_after_detection = std::round(delay_after_detection / 0.025f) * 0.025f;
|
||||
delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
|
||||
this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
|
||||
this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
|
||||
// max 32: "setLatency "(11) + float(8) + " "(1) + float(8) + null, rounded to 32
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
|
||||
this->cmd_ = buf;
|
||||
};
|
||||
|
||||
uint8_t SetLatencyCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG,
|
||||
"Updated output latency config:\n"
|
||||
" Signal that someone was detected is delayed by %.03f s.\n"
|
||||
" Signal that nobody is detected anymore is delayed by %.03f s.",
|
||||
this->delay_after_detection_, this->delay_after_disappear_);
|
||||
ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t SensorCfgStartCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot configure sensor startup behavior. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Updated sensor startup behavior:");
|
||||
if (startup_mode_) {
|
||||
this->parent_->set_start_after_boot(true);
|
||||
ESP_LOGI(TAG, "Sensor will start automatically after power-on.");
|
||||
} else {
|
||||
this->parent_->set_start_after_boot(false);
|
||||
ESP_LOGI(TAG, "Sensor needs to be started manually after power-on.");
|
||||
}
|
||||
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t FactoryResetCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot factory reset. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Sensor factory reset done.");
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t ResetSystemCommand::on_message(std::string &message) {
|
||||
if (message == "leapMMW:/>") {
|
||||
ESP_LOGI(TAG, "Restarted sensor.");
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t SaveCfgCommand::on_message(std::string &message) {
|
||||
if (message == "no parameter has changed") {
|
||||
ESP_LOGI(TAG, "Not saving config (no parameter changed).");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor.");
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t LedModeCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot set led mode. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Set led mode done.");
|
||||
if (this->active_) {
|
||||
this->parent_->set_led_active(true);
|
||||
ESP_LOGI(TAG, "Sensor LED will blink.");
|
||||
} else {
|
||||
this->parent_->set_led_active(false);
|
||||
ESP_LOGI(TAG, "Turned off LED.");
|
||||
}
|
||||
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t UartOutputCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot set uart output mode. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Set uart mode done.");
|
||||
if (this->active_) {
|
||||
this->parent_->set_uart_presence_active(true);
|
||||
ESP_LOGI(TAG, "Presence information is sent via UART and GPIO.");
|
||||
} else {
|
||||
this->parent_->set_uart_presence_active(false);
|
||||
ESP_LOGI(TAG, "Presence information is only sent via GPIO.");
|
||||
}
|
||||
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t SensitivityCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot set sensitivity. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", this->sensitivity_);
|
||||
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
@@ -1,152 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
class DfrobotSen0395Component;
|
||||
|
||||
// Use command queue and time stamps to avoid blocking.
|
||||
// When component has run time, check if minimum time (1s) between
|
||||
// commands has passed. After that run a command from the queue.
|
||||
class Command {
|
||||
public:
|
||||
virtual ~Command() = default;
|
||||
virtual uint8_t execute(DfrobotSen0395Component *parent);
|
||||
virtual uint8_t on_message(std::string &message) = 0;
|
||||
|
||||
protected:
|
||||
DfrobotSen0395Component *parent_{nullptr};
|
||||
std::string cmd_;
|
||||
bool cmd_sent_{false};
|
||||
int8_t retries_left_{2};
|
||||
uint32_t cmd_duration_ms_{1000};
|
||||
uint32_t timeout_ms_{1500};
|
||||
};
|
||||
|
||||
class ReadStateCommand : public Command {
|
||||
public:
|
||||
ReadStateCommand() { timeout_ms_ = 500; }
|
||||
uint8_t execute(DfrobotSen0395Component *parent) override;
|
||||
uint8_t on_message(std::string &message) override;
|
||||
};
|
||||
|
||||
class PowerCommand : public Command {
|
||||
public:
|
||||
PowerCommand(bool power_on) : power_on_(power_on) {
|
||||
if (power_on) {
|
||||
cmd_ = "sensorStart";
|
||||
} else {
|
||||
cmd_ = "sensorStop";
|
||||
}
|
||||
};
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
bool power_on_;
|
||||
};
|
||||
|
||||
class DetRangeCfgCommand : public Command {
|
||||
public:
|
||||
DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3, float min4, float max4);
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
float min1_, max1_, min2_, max2_, min3_, max3_, min4_, max4_;
|
||||
// TODO: Set min max values in component, so they can be published as sensor.
|
||||
};
|
||||
|
||||
class SetLatencyCommand : public Command {
|
||||
public:
|
||||
SetLatencyCommand(float delay_after_detection, float delay_after_disappear);
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
float delay_after_detection_;
|
||||
float delay_after_disappear_;
|
||||
};
|
||||
|
||||
class SensorCfgStartCommand : public Command {
|
||||
public:
|
||||
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
|
||||
char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17
|
||||
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode);
|
||||
cmd_ = std::string(tmp_cmd);
|
||||
}
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
bool startup_mode_;
|
||||
};
|
||||
|
||||
class FactoryResetCommand : public Command {
|
||||
public:
|
||||
FactoryResetCommand() { cmd_ = "factoryReset 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; };
|
||||
uint8_t on_message(std::string &message) override;
|
||||
};
|
||||
|
||||
class ResetSystemCommand : public Command {
|
||||
public:
|
||||
ResetSystemCommand() { cmd_ = "resetSystem"; }
|
||||
uint8_t on_message(std::string &message) override;
|
||||
};
|
||||
|
||||
class SaveCfgCommand : public Command {
|
||||
public:
|
||||
SaveCfgCommand() {
|
||||
cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89";
|
||||
cmd_duration_ms_ = 3000;
|
||||
timeout_ms_ = 3500;
|
||||
}
|
||||
uint8_t on_message(std::string &message) override;
|
||||
};
|
||||
|
||||
class LedModeCommand : public Command {
|
||||
public:
|
||||
LedModeCommand(bool active) : active_(active) {
|
||||
if (active) {
|
||||
cmd_ = "setLedMode 1 0";
|
||||
} else {
|
||||
cmd_ = "setLedMode 1 1";
|
||||
}
|
||||
};
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
bool active_;
|
||||
};
|
||||
|
||||
class UartOutputCommand : public Command {
|
||||
public:
|
||||
UartOutputCommand(bool active) : active_(active) {
|
||||
if (active) {
|
||||
cmd_ = "setUartOutput 1 1";
|
||||
} else {
|
||||
cmd_ = "setUartOutput 1 0";
|
||||
}
|
||||
};
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
bool active_;
|
||||
};
|
||||
|
||||
class SensitivityCommand : public Command {
|
||||
public:
|
||||
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
|
||||
if (sensitivity > 9)
|
||||
sensitivity_ = sensitivity = 9;
|
||||
char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17
|
||||
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity);
|
||||
cmd_ = std::string(tmp_cmd);
|
||||
};
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
uint8_t sensitivity_;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
@@ -1,13 +1,27 @@
|
||||
#include "dfrobot_sen0395.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395");
|
||||
const char ASCII_CR = 0x0D;
|
||||
const char ASCII_LF = 0x0A;
|
||||
static constexpr char ASCII_CR = 0x0D;
|
||||
static constexpr char ASCII_LF = 0x0A;
|
||||
static constexpr float DET_RANGE_STEP_M = 0.15f;
|
||||
static constexpr float LATENCY_STEP_S = 0.025f;
|
||||
|
||||
// What each CommandType cannot do while the sensor runs; empty for commands that do not need it stopped
|
||||
PROGMEM_STRING_TABLE(BusyActions, "", "configure range config", "configure output latency",
|
||||
"configure sensor startup behavior", "factory reset", "", "", "set led mode",
|
||||
"set uart output mode", "set sensitivity");
|
||||
static_assert(BusyActions::COUNT == static_cast<size_t>(CommandType::COMMAND_TYPE_SENSITIVITY) + 1,
|
||||
"BusyActions needs one entry per CommandType");
|
||||
|
||||
void DfrobotSen0395Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Dfrobot Mmwave Radar:");
|
||||
@@ -23,23 +37,270 @@ void DfrobotSen0395Component::dump_config() {
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::loop() {
|
||||
if (cmd_queue_.is_empty()) {
|
||||
// Command queue empty. Read sensor state.
|
||||
cmd_queue_.enqueue(make_unique<ReadStateCommand>());
|
||||
if (this->cmd_queue_.empty()) {
|
||||
this->read_state_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Commands are non-blocking and need to be called repeatedly.
|
||||
if (cmd_queue_.process(this)) {
|
||||
// Dequeue if command is done
|
||||
cmd_queue_.dequeue();
|
||||
// Commands are non-blocking and need to be called repeatedly
|
||||
if (this->run_command_(this->cmd_queue_.front(), App.get_loop_component_start_time())) {
|
||||
this->cmd_queue_.pop();
|
||||
this->cmd_sent_ = false;
|
||||
this->retries_left_ = 2;
|
||||
}
|
||||
}
|
||||
|
||||
int8_t DfrobotSen0395Component::enqueue(std::unique_ptr<Command> cmd) {
|
||||
return cmd_queue_.enqueue(std::move(cmd)); // Transfer ownership using std::move
|
||||
bool DfrobotSen0395Component::enqueue(CommandType type, uint8_t arg) {
|
||||
if (!this->cmd_queue_.push({type, arg})) {
|
||||
ESP_LOGE(TAG, "Command queue is full");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t DfrobotSen0395Component::read_message_() {
|
||||
bool DfrobotSen0395Component::has_queue_room(size_t count) const {
|
||||
if (COMMAND_QUEUE_SIZE - this->cmd_queue_.size() >= count)
|
||||
return true;
|
||||
ESP_LOGE(TAG, "Command queue is full");
|
||||
return false;
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::enqueue_det_range_cfg(float min1, float max1, float min2, float max2, float min3,
|
||||
float max3, float min4, float max4) {
|
||||
// TODO: Print warning when values are rounded
|
||||
const float values[8] = {min1, max1, min2, max2, min3, max3, min4, max4};
|
||||
int16_t steps[8];
|
||||
bool in_use = true;
|
||||
for (size_t i = 0; i < 8; i += 2) {
|
||||
in_use = in_use && values[i] >= 0 && values[i + 1] >= 0;
|
||||
for (size_t j = i; j < i + 2; j++) {
|
||||
steps[j] = in_use ? static_cast<int16_t>(clamp(roundf(values[j] / DET_RANGE_STEP_M), 0.0f, 32767.0f)) : -1;
|
||||
}
|
||||
}
|
||||
if (steps[0] < 0) {
|
||||
ESP_LOGW(TAG, "Invalid detection range, using 0 0");
|
||||
steps[0] = steps[1] = 0;
|
||||
}
|
||||
if (this->enqueue(CommandType::COMMAND_TYPE_DET_RANGE_CFG))
|
||||
memcpy(this->det_range_steps_, steps, sizeof(steps));
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::enqueue_set_latency(float delay_after_detection, float delay_after_disappear) {
|
||||
if (!this->enqueue(CommandType::COMMAND_TYPE_SET_LATENCY))
|
||||
return;
|
||||
// 1638.375 s is 65535 steps
|
||||
this->latency_steps_[0] =
|
||||
static_cast<uint16_t>(clamp(std::round(delay_after_detection / LATENCY_STEP_S), 0.0f, 65535.0f));
|
||||
this->latency_steps_[1] =
|
||||
static_cast<uint16_t>(clamp(std::round(delay_after_disappear / LATENCY_STEP_S), 0.0f, 65535.0f));
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::enqueue_saved_setting(CommandType type, bool value) {
|
||||
const bool was_active = this->active_;
|
||||
if (!this->has_queue_room(was_active ? 4 : 2))
|
||||
return;
|
||||
if (was_active)
|
||||
this->enqueue(CommandType::COMMAND_TYPE_POWER, false);
|
||||
this->enqueue(type, value);
|
||||
this->enqueue(CommandType::COMMAND_TYPE_SAVE_CFG);
|
||||
if (was_active)
|
||||
this->enqueue(CommandType::COMMAND_TYPE_POWER, true);
|
||||
}
|
||||
|
||||
bool DfrobotSen0395Component::run_command_(const Command &cmd, uint32_t now) {
|
||||
// The interval between two commands must be larger than the command duration
|
||||
const uint32_t duration = cmd.type == CommandType::COMMAND_TYPE_SAVE_CFG ? 3000 : 1000;
|
||||
if (!this->cmd_sent_) {
|
||||
if (now - this->ts_last_cmd_sent_ > duration) {
|
||||
char buf[64]; // "detRangeCfg -1" + 8 * " 32767" + null
|
||||
this->format_command(cmd, buf, sizeof(buf));
|
||||
ESP_LOGD(TAG, "Sending command: %s", buf);
|
||||
this->write_str(buf);
|
||||
this->ts_last_cmd_sent_ = now;
|
||||
this->cmd_sent_ = true;
|
||||
}
|
||||
return false; // Command not done yet
|
||||
}
|
||||
|
||||
if (this->read_message_()) {
|
||||
if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("is not recognized as a CLI command")) == nullptr) {
|
||||
if (!this->handle_reply_(cmd))
|
||||
return false; // Command not done yet
|
||||
this->read_message_(); // Skip the prompt
|
||||
return true;
|
||||
}
|
||||
ESP_LOGD(TAG, "Command not recognized properly by sensor");
|
||||
} else if (now - this->ts_last_cmd_sent_ <= duration + 500) {
|
||||
return false; // Command not done yet
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Command timeout");
|
||||
}
|
||||
|
||||
if (this->retries_left_ == 0) {
|
||||
this->read_message_(); // Skip the prompt
|
||||
return true; // Give up
|
||||
}
|
||||
this->retries_left_--;
|
||||
this->cmd_sent_ = false;
|
||||
ESP_LOGD(TAG, "Retrying");
|
||||
return false;
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::read_state_() {
|
||||
if (!this->read_message_())
|
||||
return;
|
||||
if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("$JYBSS,0, , , *")) != nullptr) {
|
||||
this->set_detected_(false);
|
||||
this->set_active(true);
|
||||
} else if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("$JYBSS,1, , , *")) != nullptr) {
|
||||
this->set_detected_(true);
|
||||
this->set_active(true);
|
||||
}
|
||||
}
|
||||
|
||||
static size_t append_cfg_key(char *buf, size_t size, size_t pos) {
|
||||
return buf_append_str(buf, size, pos, " 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89");
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::format_command(const Command &cmd, char *buf, size_t size) const {
|
||||
buf[0] = '\0';
|
||||
switch (cmd.type) {
|
||||
case CommandType::COMMAND_TYPE_POWER:
|
||||
if (cmd.arg) {
|
||||
buf_append_str(buf, size, 0, "sensorStart");
|
||||
} else {
|
||||
buf_append_str(buf, size, 0, "sensorStop");
|
||||
}
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_DET_RANGE_CFG: {
|
||||
size_t pos = buf_append_str(buf, size, 0, "detRangeCfg -1");
|
||||
for (size_t i = 0; i < 8 && this->det_range_steps_[i] >= 0; i += 2) {
|
||||
pos = buf_append_printf(buf, size, pos, " %d %d", this->det_range_steps_[i], this->det_range_steps_[i + 1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CommandType::COMMAND_TYPE_SET_LATENCY:
|
||||
buf_append_printf(buf, size, 0, "setLatency %.03f %.03f", this->latency_steps_[0] * LATENCY_STEP_S,
|
||||
this->latency_steps_[1] * LATENCY_STEP_S);
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SENSOR_CFG_START:
|
||||
buf_append_printf(buf, size, 0, "sensorCfgStart %d", cmd.arg);
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_FACTORY_RESET:
|
||||
append_cfg_key(buf, size, buf_append_str(buf, size, 0, "factoryReset"));
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
|
||||
buf_append_str(buf, size, 0, "resetSystem");
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SAVE_CFG:
|
||||
append_cfg_key(buf, size, buf_append_str(buf, size, 0, "saveCfg"));
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_LED_MODE:
|
||||
buf_append_printf(buf, size, 0, "setLedMode 1 %d", cmd.arg ? 0 : 1);
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_UART_OUTPUT:
|
||||
buf_append_printf(buf, size, 0, "setUartOutput 1 %d", cmd.arg ? 1 : 0);
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SENSITIVITY:
|
||||
buf_append_printf(buf, size, 0, "setSensitivity %d", cmd.arg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool DfrobotSen0395Component::handle_reply_(const Command &cmd) {
|
||||
const char *message = this->read_buffer_;
|
||||
auto is = [message](ESPHOME_PGM_P text) { return ESPHOME_strcmp_P(message, text) == 0; };
|
||||
|
||||
// Commands with their own replies
|
||||
switch (cmd.type) {
|
||||
case CommandType::COMMAND_TYPE_POWER:
|
||||
if (is(ESPHOME_PSTR("sensor stopped already"))) {
|
||||
this->set_active(false);
|
||||
ESP_LOGI(TAG, "Stopped sensor (already stopped)");
|
||||
} else if (is(ESPHOME_PSTR("sensor started already"))) {
|
||||
this->set_active(true);
|
||||
ESP_LOGI(TAG, "Started sensor (already started)");
|
||||
} else if (is(ESPHOME_PSTR("new parameter isn't save, can't startSensor"))) {
|
||||
this->set_active(false);
|
||||
ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)");
|
||||
} else if (is(ESPHOME_PSTR("Done"))) {
|
||||
this->set_active(cmd.arg);
|
||||
ESP_LOGI(TAG, "%s", cmd.arg ? LOG_STR_LITERAL("Started sensor") : LOG_STR_LITERAL("Stopped sensor"));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
|
||||
if (!is(ESPHOME_PSTR("leapMMW:/>")))
|
||||
return false;
|
||||
ESP_LOGI(TAG, "Restarted sensor.");
|
||||
return true;
|
||||
case CommandType::COMMAND_TYPE_SAVE_CFG:
|
||||
if (is(ESPHOME_PSTR("no parameter has changed"))) {
|
||||
ESP_LOGI(TAG, "Not saving config (no parameter changed).");
|
||||
} else if (is(ESPHOME_PSTR("Done"))) {
|
||||
ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor.");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Configuration commands, which need the sensor stopped
|
||||
if (is(ESPHOME_PSTR("sensor is not stopped"))) {
|
||||
ESP_LOGE(TAG, "Cannot %s. Sensor is not stopped!",
|
||||
LOG_STR_ARG(BusyActions::get_log_str(static_cast<uint8_t>(cmd.type), 0)));
|
||||
return true;
|
||||
}
|
||||
if (!is(ESPHOME_PSTR("Done")))
|
||||
return false;
|
||||
|
||||
switch (cmd.type) {
|
||||
case CommandType::COMMAND_TYPE_DET_RANGE_CFG:
|
||||
ESP_LOGI(TAG, "Updated detection area config:");
|
||||
for (size_t i = 0; i < 8 && this->det_range_steps_[i] >= 0; i += 2) {
|
||||
ESP_LOGI(TAG, " Detection area %u from %.02fm to %.02fm.", static_cast<unsigned>(i / 2 + 1),
|
||||
this->det_range_steps_[i] * DET_RANGE_STEP_M, this->det_range_steps_[i + 1] * DET_RANGE_STEP_M);
|
||||
}
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SET_LATENCY:
|
||||
ESP_LOGI(TAG,
|
||||
"Updated output latency config:\n"
|
||||
" Signal that someone was detected is delayed by %.03f s.\n"
|
||||
" Signal that nobody is detected anymore is delayed by %.03f s.",
|
||||
this->latency_steps_[0] * LATENCY_STEP_S, this->latency_steps_[1] * LATENCY_STEP_S);
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SENSOR_CFG_START:
|
||||
this->set_start_after_boot(cmd.arg);
|
||||
ESP_LOGI(TAG, "Updated sensor startup behavior: %s",
|
||||
cmd.arg ? LOG_STR_LITERAL("sensor will start automatically after power-on.")
|
||||
: LOG_STR_LITERAL("sensor needs to be started manually after power-on."));
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_FACTORY_RESET:
|
||||
ESP_LOGI(TAG, "Sensor factory reset done.");
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_LED_MODE:
|
||||
this->set_led_active(cmd.arg);
|
||||
ESP_LOGI(TAG, "Set led mode done. %s",
|
||||
cmd.arg ? LOG_STR_LITERAL("Sensor LED will blink.") : LOG_STR_LITERAL("Turned off LED."));
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_UART_OUTPUT:
|
||||
this->set_uart_presence_active(cmd.arg);
|
||||
ESP_LOGI(TAG, "Set uart mode done. %s",
|
||||
cmd.arg ? LOG_STR_LITERAL("Presence information is sent via UART and GPIO.")
|
||||
: LOG_STR_LITERAL("Presence information is only sent via GPIO."));
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SENSITIVITY:
|
||||
ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", cmd.arg);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DfrobotSen0395Component::read_message_() {
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
@@ -62,33 +323,11 @@ uint8_t DfrobotSen0395Component::read_message_() {
|
||||
this->read_buffer_[this->read_pos_] = 0;
|
||||
this->read_pos_ = 0;
|
||||
ESP_LOGV(TAG, "Message: %s", this->read_buffer_);
|
||||
return 1; // Full message in buffer
|
||||
} else {
|
||||
this->read_pos_++;
|
||||
return true; // Full message in buffer
|
||||
}
|
||||
this->read_pos_++;
|
||||
}
|
||||
return 0; // No full message yet
|
||||
}
|
||||
|
||||
uint8_t DfrobotSen0395Component::find_prompt_() {
|
||||
if (this->read_message_()) {
|
||||
std::string message(this->read_buffer_);
|
||||
if (message.rfind("leapMMW:/>") != std::string::npos) {
|
||||
return 1; // Prompt found
|
||||
}
|
||||
}
|
||||
return 0; // Not found yet
|
||||
}
|
||||
|
||||
uint8_t DfrobotSen0395Component::send_cmd_(const char *cmd, uint32_t duration) {
|
||||
// The interval between two commands must be larger than the specified duration (in ms).
|
||||
if (millis() - ts_last_cmd_sent_ > duration) {
|
||||
this->write_str(cmd);
|
||||
ts_last_cmd_sent_ = millis();
|
||||
return 1; // Command sent
|
||||
}
|
||||
// Could not send command yet as command duration did not fully pass yet.
|
||||
return 0;
|
||||
return false; // No full message yet
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::set_detected_(bool detected) {
|
||||
@@ -99,44 +338,4 @@ void DfrobotSen0395Component::set_detected_(bool detected) {
|
||||
#endif
|
||||
}
|
||||
|
||||
int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) {
|
||||
if (this->is_full()) {
|
||||
ESP_LOGE(TAG, "Command queue is full");
|
||||
return -1;
|
||||
} else if (this->is_empty()) {
|
||||
front_++;
|
||||
}
|
||||
rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE;
|
||||
commands_[rear_] = std::move(cmd); // Transfer ownership using std::move
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::unique_ptr<Command> CircularCommandQueue::dequeue() {
|
||||
if (this->is_empty())
|
||||
return nullptr;
|
||||
std::unique_ptr<Command> dequeued_cmd = std::move(commands_[front_]);
|
||||
if (front_ == rear_) {
|
||||
front_ = -1;
|
||||
rear_ = -1;
|
||||
} else {
|
||||
front_ = (front_ + 1) % COMMAND_QUEUE_SIZE;
|
||||
}
|
||||
|
||||
return dequeued_cmd;
|
||||
}
|
||||
|
||||
bool CircularCommandQueue::is_empty() { return front_ == -1; }
|
||||
|
||||
bool CircularCommandQueue::is_full() { return (rear_ + 1) % COMMAND_QUEUE_SIZE == front_; }
|
||||
|
||||
// Run execute method of first in line command.
|
||||
// Execute is non-blocking and has to be called until it returns 1.
|
||||
uint8_t CircularCommandQueue::process(DfrobotSen0395Component *parent) {
|
||||
if (!is_empty()) {
|
||||
return commands_[front_]->execute(parent);
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
@@ -11,29 +12,30 @@
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
#include "commands.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
const uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
|
||||
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
|
||||
static constexpr uint8_t COMMAND_QUEUE_SIZE = 20;
|
||||
|
||||
// forward declaration due to circular dependency
|
||||
class DfrobotSen0395Component;
|
||||
// Keep in sync with the BusyActions table in dfrobot_sen0395.cpp
|
||||
enum class CommandType : uint8_t {
|
||||
COMMAND_TYPE_POWER,
|
||||
COMMAND_TYPE_DET_RANGE_CFG,
|
||||
COMMAND_TYPE_SET_LATENCY,
|
||||
COMMAND_TYPE_SENSOR_CFG_START,
|
||||
COMMAND_TYPE_FACTORY_RESET,
|
||||
COMMAND_TYPE_RESET_SYSTEM,
|
||||
COMMAND_TYPE_SAVE_CFG,
|
||||
COMMAND_TYPE_LED_MODE,
|
||||
COMMAND_TYPE_UART_OUTPUT,
|
||||
COMMAND_TYPE_SENSITIVITY,
|
||||
};
|
||||
|
||||
static const uint8_t COMMAND_QUEUE_SIZE = 20;
|
||||
|
||||
class CircularCommandQueue {
|
||||
public:
|
||||
int8_t enqueue(std::unique_ptr<Command> cmd);
|
||||
std::unique_ptr<Command> dequeue();
|
||||
bool is_empty();
|
||||
bool is_full();
|
||||
uint8_t process(DfrobotSen0395Component *parent);
|
||||
|
||||
protected:
|
||||
int front_{-1};
|
||||
int rear_{-1};
|
||||
std::unique_ptr<Command> commands_[COMMAND_QUEUE_SIZE];
|
||||
/// A queued command. arg is the on/off flag, startup mode or sensitivity; the detection range and
|
||||
/// latency values are held by the component.
|
||||
struct Command {
|
||||
CommandType type;
|
||||
uint8_t arg;
|
||||
};
|
||||
|
||||
class DfrobotSen0395Component final : public uart::UARTDevice, public Component {
|
||||
@@ -93,9 +95,21 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
|
||||
}
|
||||
#endif
|
||||
|
||||
int8_t enqueue(std::unique_ptr<Command> cmd);
|
||||
// Commands run one at a time from a queue, at least 1 s apart, so loop() never blocks
|
||||
bool enqueue(CommandType type, uint8_t arg = 0);
|
||||
/// True when count more commands fit; logs when they do not, so a stop/start sequence is never queued in part
|
||||
bool has_queue_room(size_t count) const;
|
||||
/// Queue a detection range config; segments are used up to the first one with a negative bound
|
||||
void enqueue_det_range_cfg(float min1, float max1, float min2, float max2, float min3, float max3, float min4,
|
||||
float max4);
|
||||
/// Queue an output latency config, both delays in seconds
|
||||
void enqueue_set_latency(float delay_after_detection, float delay_after_disappear);
|
||||
/// Queue a setting change, stopping the sensor around it and saving it when the sensor is running
|
||||
void enqueue_saved_setting(CommandType type, bool value);
|
||||
/// Queue a sensor reset
|
||||
void reset_system() { this->enqueue(make_unique<ResetSystemCommand>()); }
|
||||
void reset_system() { this->enqueue(CommandType::COMMAND_TYPE_RESET_SYSTEM); }
|
||||
/// Write the text sent to the sensor for cmd into buf
|
||||
void format_command(const Command &cmd, char *buf, size_t size) const;
|
||||
|
||||
protected:
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -109,17 +123,22 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
|
||||
bool start_after_boot_{false};
|
||||
char read_buffer_[MMWAVE_READ_BUFFER_LENGTH];
|
||||
size_t read_pos_{0};
|
||||
CircularCommandQueue cmd_queue_;
|
||||
bool cmd_sent_{false};
|
||||
uint8_t retries_left_{2};
|
||||
StaticRingBuffer<Command, COMMAND_QUEUE_SIZE> cmd_queue_;
|
||||
uint32_t ts_last_cmd_sent_{0};
|
||||
// Range and latency of the queued commands, in sensor steps (0.15 m, 0.025 s); a later command overwrites them
|
||||
int16_t det_range_steps_[8]{};
|
||||
uint16_t latency_steps_[2]{};
|
||||
|
||||
uint8_t read_message_();
|
||||
uint8_t find_prompt_();
|
||||
uint8_t send_cmd_(const char *cmd, uint32_t duration);
|
||||
bool read_message_();
|
||||
void read_state_();
|
||||
/// Run the command at the front of the queue; true when it is done
|
||||
bool run_command_(const Command &cmd, uint32_t now);
|
||||
/// Handle a reply to a sent command; true when the command is done
|
||||
bool handle_reply_(const Command &cmd);
|
||||
|
||||
void set_detected_(bool detected);
|
||||
|
||||
friend class Command;
|
||||
friend class ReadStateCommand;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
@@ -2,63 +2,49 @@ import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component
|
||||
from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component, dfrobot_sen0395_ns
|
||||
|
||||
DEPENDENCIES = ["dfrobot_sen0395"]
|
||||
|
||||
dfrobot_sen0395_ns = cg.esphome_ns.namespace("dfrobot_sen0395")
|
||||
DfrobotSen0395Switch = dfrobot_sen0395_ns.class_(
|
||||
"DfrobotSen0395Switch",
|
||||
switch.Switch,
|
||||
cg.Component,
|
||||
cg.Parented.template(DfrobotSen0395Component),
|
||||
)
|
||||
CommandType = dfrobot_sen0395_ns.enum("CommandType", is_class=True)
|
||||
|
||||
Sen0395PowerSwitch = dfrobot_sen0395_ns.class_(
|
||||
"Sen0395PowerSwitch", DfrobotSen0395Switch
|
||||
)
|
||||
Sen0395LedSwitch = dfrobot_sen0395_ns.class_("Sen0395LedSwitch", DfrobotSen0395Switch)
|
||||
Sen0395UartPresenceSwitch = dfrobot_sen0395_ns.class_(
|
||||
"Sen0395UartPresenceSwitch", DfrobotSen0395Switch
|
||||
)
|
||||
Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_(
|
||||
"Sen0395StartAfterBootSwitch", DfrobotSen0395Switch
|
||||
)
|
||||
|
||||
|
||||
def _switch_schema(class_: MockObjClass) -> cv.Schema:
|
||||
return (
|
||||
switch.switch_schema(
|
||||
class_,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(
|
||||
DfrobotSen0395Component
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.typed_schema(
|
||||
_SWITCH_SCHEMA = switch.switch_schema(
|
||||
switch.ApplySwitch,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
).extend(
|
||||
{
|
||||
"sensor_active": _switch_schema(Sen0395PowerSwitch),
|
||||
"turn_on_led": _switch_schema(Sen0395LedSwitch),
|
||||
"presence_via_uart": _switch_schema(Sen0395UartPresenceSwitch),
|
||||
"start_after_boot": _switch_schema(Sen0395StartAfterBootSwitch),
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component),
|
||||
}
|
||||
)
|
||||
|
||||
# Per type: the hub setter that stores the switch, and the command its writes queue
|
||||
_SWITCHES = {
|
||||
"sensor_active": (
|
||||
lambda hub: hub.set_sensor_active_switch,
|
||||
f"enqueue({CommandType.COMMAND_TYPE_POWER}, state)",
|
||||
),
|
||||
"turn_on_led": (
|
||||
lambda hub: hub.set_turn_on_led_switch,
|
||||
f"enqueue_saved_setting({CommandType.COMMAND_TYPE_LED_MODE}, state)",
|
||||
),
|
||||
"presence_via_uart": (
|
||||
lambda hub: hub.set_presence_via_uart_switch,
|
||||
f"enqueue_saved_setting({CommandType.COMMAND_TYPE_UART_OUTPUT}, state)",
|
||||
),
|
||||
"start_after_boot": (
|
||||
lambda hub: hub.set_start_after_boot_switch,
|
||||
f"enqueue_saved_setting({CommandType.COMMAND_TYPE_SENSOR_CFG_START}, state)",
|
||||
),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = cv.typed_schema(dict.fromkeys(_SWITCHES, _SWITCH_SCHEMA))
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID])
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, parent)
|
||||
cg.add(getattr(parent, f"set_{config[CONF_TYPE]}_switch")(var))
|
||||
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID])
|
||||
setter, apply = _SWITCHES[config[CONF_TYPE]]
|
||||
await switch.sub_switches(config, parent=hub).create(
|
||||
config, setter(hub), apply=apply
|
||||
)
|
||||
@@ -1,46 +0,0 @@
|
||||
#include "dfrobot_sen0395_switch.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(make_unique<PowerCommand>(state)); }
|
||||
|
||||
void Sen0395LedSwitch::write_state(bool state) {
|
||||
bool was_active = false;
|
||||
if (this->parent_->is_active()) {
|
||||
was_active = true;
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(false));
|
||||
}
|
||||
this->parent_->enqueue(make_unique<LedModeCommand>(state));
|
||||
this->parent_->enqueue(make_unique<SaveCfgCommand>());
|
||||
if (was_active) {
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(true));
|
||||
}
|
||||
}
|
||||
|
||||
void Sen0395UartPresenceSwitch::write_state(bool state) {
|
||||
bool was_active = false;
|
||||
if (this->parent_->is_active()) {
|
||||
was_active = true;
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(false));
|
||||
}
|
||||
this->parent_->enqueue(make_unique<UartOutputCommand>(state));
|
||||
this->parent_->enqueue(make_unique<SaveCfgCommand>());
|
||||
if (was_active) {
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(true));
|
||||
}
|
||||
}
|
||||
|
||||
void Sen0395StartAfterBootSwitch::write_state(bool state) {
|
||||
bool was_active = false;
|
||||
if (this->parent_->is_active()) {
|
||||
was_active = true;
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(false));
|
||||
}
|
||||
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(state));
|
||||
this->parent_->enqueue(make_unique<SaveCfgCommand>());
|
||||
if (was_active) {
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(true));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include "../dfrobot_sen0395.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
class DfrobotSen0395Switch : public switch_::Switch, public Component, public Parented<DfrobotSen0395Component> {};
|
||||
|
||||
class Sen0395PowerSwitch final : public DfrobotSen0395Switch {
|
||||
public:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
class Sen0395LedSwitch final : public DfrobotSen0395Switch {
|
||||
public:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
class Sen0395UartPresenceSwitch final : public DfrobotSen0395Switch {
|
||||
public:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
class Sen0395StartAfterBootSwitch final : public DfrobotSen0395Switch {
|
||||
public:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
@@ -34,10 +34,7 @@ DFRobotSEN0465Component = dfrobot_sen0465_base_ns.class_(
|
||||
"DFRobotSEN0465Component", cg.PollingComponent
|
||||
)
|
||||
AlarmType = dfrobot_sen0465_base_ns.enum("AlarmType", is_class=True)
|
||||
ALARM_TYPES = {
|
||||
"LOW": AlarmType.ALARM_TYPE_LOW,
|
||||
"HIGH": AlarmType.ALARM_TYPE_HIGH,
|
||||
}
|
||||
ALARM_TYPES = AlarmType.options("LOW", "HIGH", prefix="ALARM_TYPE_")
|
||||
|
||||
# One piece of the temperature compensation from the DFRobot_MultiGasSensor library:
|
||||
# for low < T <= high, compensated = raw / (a * T + b) - (c * T + d)
|
||||
|
||||
@@ -72,10 +72,7 @@ void DFRobotSEN0465Component::dump_config() {
|
||||
if (this->alarm_type_ == AlarmType::ALARM_TYPE_OFF) {
|
||||
ESP_LOGCONFIG(TAG, " Alarm: off");
|
||||
} else {
|
||||
float scale = 1.0f;
|
||||
for (uint8_t i = 0; i < this->alarm_decimals_; i++)
|
||||
scale *= 10.0f;
|
||||
const float threshold = this->alarm_threshold_ / scale;
|
||||
const float threshold = this->alarm_threshold_ / pow10_int(this->alarm_decimals_);
|
||||
ESP_LOGCONFIG(TAG, " Alarm: %s %.*f",
|
||||
this->alarm_type_ == AlarmType::ALARM_TYPE_HIGH ? LOG_STR_LITERAL("above") : LOG_STR_LITERAL("below"),
|
||||
this->alarm_decimals_, threshold);
|
||||
@@ -83,9 +80,6 @@ void DFRobotSEN0465Component::dump_config() {
|
||||
if (this->setup_incomplete_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup incomplete: yes");
|
||||
}
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Concentration", this->concentration_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
@@ -228,23 +222,14 @@ void DFRobotSEN0465Component::handle_data_(const uint8_t *frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
float concentration = encode_uint16(frame[2], frame[3]);
|
||||
switch (frame[5]) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
concentration *= 0.1f;
|
||||
break;
|
||||
case 2:
|
||||
concentration *= 0.01f;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Invalid number of decimal places: %u", frame[5]);
|
||||
if (this->concentration_sensor_ != nullptr)
|
||||
this->concentration_sensor_->publish_state(NAN);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
if (frame[5] > 2) {
|
||||
ESP_LOGW(TAG, "Invalid number of decimal places: %u", frame[5]);
|
||||
if (this->concentration_sensor_ != nullptr)
|
||||
this->concentration_sensor_->publish_state(NAN);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
float concentration = encode_uint16(frame[2], frame[3]) * pow10_int(-frame[5]);
|
||||
|
||||
#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
|
||||
if (!this->compensation_.empty())
|
||||
|
||||
@@ -95,12 +95,6 @@ static bool answer_means_done(Operation operation, const char *line) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool parse_float(const char *str, float &value) {
|
||||
char *end;
|
||||
value = strtof(str, &end);
|
||||
return end != str && *end == '\0';
|
||||
}
|
||||
|
||||
static const LogString *operation_to_str(Operation operation) {
|
||||
switch (operation) {
|
||||
case Operation::OPERATION_STOP:
|
||||
@@ -173,8 +167,8 @@ void DFRobotSEN0609Component::setup() {
|
||||
switch_::Switch *sw = this->switches_[i];
|
||||
if (sw == nullptr)
|
||||
continue;
|
||||
const bool on = sw->get_initial_state_with_restore_mode().value_or(true);
|
||||
this->publish_switch_(static_cast<SwitchSetting>(i), on != sw->is_inverted());
|
||||
// A disabled restore mode leaves the switch on, which an inverted switch writes as off
|
||||
this->publish_switch_(static_cast<SwitchSetting>(i), sw->get_initial_write_state().value_or(!sw->is_inverted()));
|
||||
}
|
||||
#endif
|
||||
this->queue_configuration_();
|
||||
@@ -286,9 +280,6 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
|
||||
return;
|
||||
}
|
||||
char *fields[FRAME_FIELDS];
|
||||
float distance;
|
||||
float speed;
|
||||
float energy;
|
||||
if (split_fields(line + 7, fields, FRAME_FIELDS) < FRAME_FIELDS) {
|
||||
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
|
||||
return;
|
||||
@@ -308,7 +299,10 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
if (!parse_float(fields[2], distance) || !parse_float(fields[3], speed) || !parse_float(fields[4], energy)) {
|
||||
const optional<float> distance = parse_number<float>(fields[2]);
|
||||
const optional<float> speed = parse_number<float>(fields[3]);
|
||||
const optional<float> energy = parse_number<float>(fields[4]);
|
||||
if (!distance.has_value() || !speed.has_value() || !energy.has_value()) {
|
||||
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
|
||||
return;
|
||||
}
|
||||
@@ -317,7 +311,7 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
|
||||
this->has_target_binary_sensor_->publish_state(true);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
this->publish_target_(distance, speed, energy);
|
||||
this->publish_target_(*distance, *speed, *energy);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_EMC2101_ID, EMC2101_COMPONENT_SCHEMA, emc2101_ns
|
||||
from .. import CONF_EMC2101_ID, EMC2101_COMPONENT_SCHEMA
|
||||
|
||||
DEPENDENCIES = ["emc2101"]
|
||||
|
||||
EMC2101Output = emc2101_ns.class_("EMC2101Output", output.FloatOutput)
|
||||
|
||||
CONFIG_SCHEMA = EMC2101_COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(EMC2101Output),
|
||||
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
paren = await cg.get_variable(config[CONF_EMC2101_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID], paren)
|
||||
await output.register_output(var, config)
|
||||
await output.sub_float_outputs(config, parent=config[CONF_EMC2101_ID]).create(
|
||||
config, None, apply="set_duty_cycle(state)"
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
#include "emc2101_output.h"
|
||||
|
||||
namespace esphome::emc2101 {
|
||||
|
||||
void EMC2101Output::write_state(float state) { this->parent_->set_duty_cycle(state); }
|
||||
|
||||
} // namespace esphome::emc2101
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../emc2101.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
|
||||
namespace esphome::emc2101 {
|
||||
|
||||
/// This class allows to control the EMC2101 output.
|
||||
class EMC2101Output final : public output::FloatOutput {
|
||||
public:
|
||||
EMC2101Output(Emc2101Component *parent) : parent_(parent) {}
|
||||
|
||||
protected:
|
||||
/** Used by ESPHome framework. */
|
||||
void write_state(float state) override;
|
||||
|
||||
Emc2101Component *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::emc2101
|
||||
@@ -21,6 +21,23 @@ ESPHOME_LOG_TAG(TAG, "es8311");
|
||||
return false; \
|
||||
}
|
||||
|
||||
static constexpr i2c::I2CRegValue ES8311_POWER_UP[] PROGMEM = {
|
||||
// Power up analog circuitry
|
||||
{ES8311_REG0D_SYSTEM, 0x01},
|
||||
// Enable analog PGA, enable ADC modulator
|
||||
{ES8311_REG0E_SYSTEM, 0x02},
|
||||
// Power up DAC
|
||||
{ES8311_REG12_SYSTEM, 0x00},
|
||||
// Enable output to HP drive
|
||||
{ES8311_REG13_SYSTEM, 0x10},
|
||||
// ADC Equalizer bypass, cancel DC offset in digital domain
|
||||
{ES8311_REG1C_ADC, 0x6A},
|
||||
// Bypass DAC equalizer
|
||||
{ES8311_REG37_DAC, 0x08},
|
||||
// Power On
|
||||
{ES8311_REG00_RESET, 0x80},
|
||||
};
|
||||
|
||||
void ES8311::setup() {
|
||||
// Reset
|
||||
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x1F));
|
||||
@@ -33,20 +50,7 @@ void ES8311::setup() {
|
||||
// Set initial volume
|
||||
this->set_volume(0.75); // 0.75 = 0xBF = 0dB
|
||||
|
||||
// Power up analog circuitry
|
||||
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0D_SYSTEM, 0x01));
|
||||
// Enable analog PGA, enable ADC modulator
|
||||
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0E_SYSTEM, 0x02));
|
||||
// Power up DAC
|
||||
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG12_SYSTEM, 0x00));
|
||||
// Enable output to HP drive
|
||||
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG13_SYSTEM, 0x10));
|
||||
// ADC Equalizer bypass, cancel DC offset in digital domain
|
||||
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG1C_ADC, 0x6A));
|
||||
// Bypass DAC equalizer
|
||||
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG37_DAC, 0x08));
|
||||
// Power On
|
||||
ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x80));
|
||||
ES8311_ERROR_FAILED(this->write_registers(ES8311_POWER_UP));
|
||||
}
|
||||
|
||||
void ES8311::dump_config() {
|
||||
|
||||
@@ -174,21 +174,11 @@ float ES8388::volume() {
|
||||
}
|
||||
|
||||
bool ES8388::set_mute_state_(bool mute_state) {
|
||||
uint8_t value = 0;
|
||||
|
||||
this->is_muted_ = mute_state;
|
||||
|
||||
ES8388_ERROR_CHECK(this->read_byte(ES8388_DACCONTROL3, &value));
|
||||
ESP_LOGV(TAG, "Read ES8388_DACCONTROL3: 0x%02X", value);
|
||||
|
||||
// Only toggle the DACMute bit; the other bits of this register hold unrelated
|
||||
// DAC settings that must be preserved. Previously muting overwrote the whole
|
||||
// register with 0x3C and unmuting never cleared the bit, so once muted the DAC
|
||||
// could not be unmuted again.
|
||||
assign_bits(value, ES8388_DACCONTROL3_DAC_MUTE, mute_state);
|
||||
|
||||
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
|
||||
return this->write_byte(ES8388_DACCONTROL3, value);
|
||||
// Only toggle the DACMute bit; the other bits of this register hold unrelated DAC settings
|
||||
return this->update_register(ES8388_DACCONTROL3, ES8388_DACCONTROL3_DAC_MUTE,
|
||||
mute_state ? ES8388_DACCONTROL3_DAC_MUTE : 0);
|
||||
}
|
||||
|
||||
// Set dac power output
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -41,23 +39,20 @@ from .const import (
|
||||
VARIANT_ESP32S31,
|
||||
esp32_ns,
|
||||
)
|
||||
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
|
||||
from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_supports
|
||||
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
|
||||
from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_supports
|
||||
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
|
||||
from .gpio_esp32_c61 import esp32_c61_validate_gpio_pin, esp32_c61_validate_supports
|
||||
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
|
||||
from .gpio_esp32_h4 import esp32_h4_validate_gpio_pin, esp32_h4_validate_supports
|
||||
from .gpio_esp32_h21 import esp32_h21_validate_gpio_pin, esp32_h21_validate_supports
|
||||
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
|
||||
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
|
||||
from .gpio_esp32_s3 import (
|
||||
esp32_s3_final_validate_pins,
|
||||
esp32_s3_validate_gpio_pin,
|
||||
esp32_s3_validate_supports,
|
||||
)
|
||||
from .gpio_esp32_s31 import esp32_s31_validate_gpio_pin, esp32_s31_validate_supports
|
||||
from .gpio_common import ESP32PinSpec
|
||||
from .gpio_esp32 import ESP32_PIN_SPEC
|
||||
from .gpio_esp32_c2 import ESP32_C2_PIN_SPEC
|
||||
from .gpio_esp32_c3 import ESP32_C3_PIN_SPEC
|
||||
from .gpio_esp32_c5 import ESP32_C5_PIN_SPEC
|
||||
from .gpio_esp32_c6 import ESP32_C6_PIN_SPEC
|
||||
from .gpio_esp32_c61 import ESP32_C61_PIN_SPEC
|
||||
from .gpio_esp32_h2 import ESP32_H2_PIN_SPEC
|
||||
from .gpio_esp32_h4 import ESP32_H4_PIN_SPEC
|
||||
from .gpio_esp32_h21 import ESP32_H21_PIN_SPEC
|
||||
from .gpio_esp32_p4 import ESP32_P4_PIN_SPEC
|
||||
from .gpio_esp32_s2 import ESP32_S2_PIN_SPEC
|
||||
from .gpio_esp32_s3 import ESP32_S3_PIN_SPEC, esp32_s3_final_validate_pins
|
||||
from .gpio_esp32_s31 import ESP32_S31_PIN_SPEC
|
||||
|
||||
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
|
||||
|
||||
@@ -98,66 +93,23 @@ def _translate_pin(value):
|
||||
return _lookup_pin(value)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ESP32ValidationFunctions:
|
||||
pin_validation: Callable[[int], int]
|
||||
usage_validation: Callable[[dict[str, Any]], dict[str, Any]]
|
||||
final_validate: Callable[[ConfigType], None] | None = None
|
||||
|
||||
|
||||
_esp32_validations = {
|
||||
VARIANT_ESP32: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports
|
||||
),
|
||||
VARIANT_ESP32C2: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_c2_validate_gpio_pin,
|
||||
usage_validation=esp32_c2_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32C3: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_c3_validate_gpio_pin,
|
||||
usage_validation=esp32_c3_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32C5: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_c5_validate_gpio_pin,
|
||||
usage_validation=esp32_c5_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32C6: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_c6_validate_gpio_pin,
|
||||
usage_validation=esp32_c6_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32C61: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_c61_validate_gpio_pin,
|
||||
usage_validation=esp32_c61_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32H2: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_h2_validate_gpio_pin,
|
||||
usage_validation=esp32_h2_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32H4: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_h4_validate_gpio_pin,
|
||||
usage_validation=esp32_h4_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32H21: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_h21_validate_gpio_pin,
|
||||
usage_validation=esp32_h21_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32P4: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_p4_validate_gpio_pin,
|
||||
usage_validation=esp32_p4_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32S2: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_s2_validate_gpio_pin,
|
||||
usage_validation=esp32_s2_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32S3: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_s3_validate_gpio_pin,
|
||||
usage_validation=esp32_s3_validate_supports,
|
||||
final_validate=esp32_s3_final_validate_pins,
|
||||
),
|
||||
VARIANT_ESP32S31: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_s31_validate_gpio_pin,
|
||||
usage_validation=esp32_s31_validate_supports,
|
||||
),
|
||||
_esp32_pin_specs: dict[str, ESP32PinSpec] = {
|
||||
VARIANT_ESP32: ESP32_PIN_SPEC,
|
||||
VARIANT_ESP32C2: ESP32_C2_PIN_SPEC,
|
||||
VARIANT_ESP32C3: ESP32_C3_PIN_SPEC,
|
||||
VARIANT_ESP32C5: ESP32_C5_PIN_SPEC,
|
||||
VARIANT_ESP32C6: ESP32_C6_PIN_SPEC,
|
||||
VARIANT_ESP32C61: ESP32_C61_PIN_SPEC,
|
||||
VARIANT_ESP32H2: ESP32_H2_PIN_SPEC,
|
||||
VARIANT_ESP32H4: ESP32_H4_PIN_SPEC,
|
||||
VARIANT_ESP32H21: ESP32_H21_PIN_SPEC,
|
||||
VARIANT_ESP32P4: ESP32_P4_PIN_SPEC,
|
||||
VARIANT_ESP32S2: ESP32_S2_PIN_SPEC,
|
||||
VARIANT_ESP32S3: ESP32_S3_PIN_SPEC,
|
||||
VARIANT_ESP32S31: ESP32_S31_PIN_SPEC,
|
||||
}
|
||||
_final_validations: dict[str, Callable[[ConfigType], None]] = {
|
||||
VARIANT_ESP32S3: esp32_s3_final_validate_pins,
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +126,7 @@ def gpio_pin_number_validator(value):
|
||||
return value
|
||||
|
||||
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
|
||||
if variant not in _esp32_validations:
|
||||
if variant not in _esp32_pin_specs:
|
||||
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
|
||||
|
||||
return value
|
||||
@@ -182,12 +134,12 @@ def gpio_pin_number_validator(value):
|
||||
|
||||
def validate_gpio_pin(pin):
|
||||
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
|
||||
if variant not in _esp32_validations:
|
||||
if variant not in _esp32_pin_specs:
|
||||
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
|
||||
|
||||
ignore_pin_validation_warning = pin[CONF_IGNORE_PIN_VALIDATION_ERROR]
|
||||
try:
|
||||
pin[CONF_NUMBER] = _esp32_validations[variant].pin_validation(pin[CONF_NUMBER])
|
||||
pin[CONF_NUMBER] = _esp32_pin_specs[variant].validate_gpio_pin(pin[CONF_NUMBER])
|
||||
except cv.Invalid as exc:
|
||||
if not ignore_pin_validation_warning:
|
||||
raise
|
||||
@@ -198,10 +150,10 @@ def validate_gpio_pin(pin):
|
||||
exc,
|
||||
)
|
||||
else:
|
||||
# `ignore_pin_validation_error` only suppresses an error raised by the
|
||||
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
|
||||
# numbers). If that didn't raise, the option is a no-op -- warn so the
|
||||
# user can clean it up, but don't block the build.
|
||||
# `ignore_pin_validation_error` only suppresses an error raised by
|
||||
# validate_gpio_pin above (e.g. SPI flash/PSRAM pins); pin numbers out of
|
||||
# range still fail validate_supports. If nothing raised, the option is a
|
||||
# no-op -- warn so the user can clean it up, but don't block the build.
|
||||
if ignore_pin_validation_warning:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d has no validation errors to ignore; "
|
||||
@@ -217,7 +169,7 @@ def validate_supports(value):
|
||||
is_output = mode[CONF_OUTPUT]
|
||||
is_open_drain = mode[CONF_OPEN_DRAIN]
|
||||
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
|
||||
if variant not in _esp32_validations:
|
||||
if variant not in _esp32_pin_specs:
|
||||
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
|
||||
|
||||
if is_open_drain and not is_output:
|
||||
@@ -225,7 +177,7 @@ def validate_supports(value):
|
||||
"Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN]
|
||||
)
|
||||
|
||||
return _esp32_validations[variant].usage_validation(value)
|
||||
return _esp32_pin_specs[variant].validate_supports(value)
|
||||
|
||||
|
||||
# https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t
|
||||
@@ -276,6 +228,5 @@ async def esp32_pin_to_code(config):
|
||||
|
||||
def final_validate_pins(full_config: ConfigType) -> None:
|
||||
"""Run the active variant's pin final-validation, if it defines one."""
|
||||
funcs = _esp32_validations.get(CORE.data[KEY_ESP32][KEY_VARIANT])
|
||||
if funcs is not None and funcs.final_validate is not None:
|
||||
funcs.final_validate(full_config)
|
||||
if (final := _final_validations.get(CORE.data[KEY_ESP32][KEY_VARIANT])) is not None:
|
||||
final(full_config)
|
||||
@@ -1,26 +1,129 @@
|
||||
"""Pin checks shared by the ESP32 variant validators."""
|
||||
|
||||
from logging import Logger
|
||||
from typing import Any
|
||||
from collections.abc import Iterable, Mapping
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
from esphome.const import CONF_NUMBER
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_OUTPUT,
|
||||
CONF_PULLDOWN,
|
||||
CONF_PULLUP,
|
||||
)
|
||||
from esphome.pins import check_strapping_pin
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_NO_PINS: Mapping[int, str] = MappingProxyType({})
|
||||
|
||||
_INPUT_ONLY_REJECTED_MODES = (
|
||||
(CONF_OUTPUT, "does not support output pin mode."),
|
||||
(CONF_PULLUP, "does not support pullups."),
|
||||
(CONF_PULLDOWN, "does not support pulldowns."),
|
||||
)
|
||||
|
||||
|
||||
def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> None:
|
||||
if num in usb_jtag_pins:
|
||||
logger.warning(
|
||||
"GPIO%d is used by the USB-Serial-JTAG interface."
|
||||
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
|
||||
num,
|
||||
)
|
||||
def spi_psram_errors(variant: str, functions: Mapping[int, str]) -> dict[int, str]:
|
||||
"""Error messages for pins taken by the SPI flash/PSRAM interface."""
|
||||
return {
|
||||
num: f"This pin cannot be used on {variant}s and is already used by the"
|
||||
f" SPI/PSRAM interface (function: {function})"
|
||||
for num, function in functions.items()
|
||||
}
|
||||
|
||||
|
||||
def check_usb_jtag_hold(
|
||||
conf: dict[str, Any], usb_jtag_pins: set[int], logger: Logger
|
||||
) -> None:
|
||||
num = conf[CONF_NUMBER]
|
||||
if conf.get(CONF_HOLD_STATE) and num in usb_jtag_pins:
|
||||
logger.warning(
|
||||
"GPIO%d cannot hold at low level during wakeup from deep sleep.", num
|
||||
)
|
||||
def unusable_errors(variant: str, pins: Iterable[int]) -> dict[int, str]:
|
||||
"""Error messages for pins that are missing from the GPIO matrix."""
|
||||
return {num: f"The pin GPIO{num} is not usable on {variant}s." for num in pins}
|
||||
|
||||
|
||||
class ESP32PinSpec:
|
||||
"""Pin facts of one ESP32 variant, with the two validators built from them.
|
||||
|
||||
`error_pins` fail pin validation, which `ignore_pin_validation_error` bypasses;
|
||||
`fatal_pins` and pins above `max_pin` fail both validators, so the option cannot bypass them.
|
||||
`input_only_label` is inserted after "GPIOn" in the input-only errors.
|
||||
A plain class, not a dataclass: building a dataclass costs ~0.5 ms of import.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"error_pins",
|
||||
"fatal_pins",
|
||||
"input_only_label",
|
||||
"input_only_pins",
|
||||
"max_pin",
|
||||
"no_hold_pins",
|
||||
"strapping_pins",
|
||||
"usb_jtag_pins",
|
||||
"warning_pins",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
max_pin: int,
|
||||
strapping_pins: frozenset[int],
|
||||
usb_jtag_pins: frozenset[int] = frozenset(),
|
||||
error_pins: Mapping[int, str] = _NO_PINS,
|
||||
fatal_pins: Mapping[int, str] = _NO_PINS,
|
||||
warning_pins: Mapping[int, str] = _NO_PINS,
|
||||
input_only_pins: frozenset[int] = frozenset(),
|
||||
input_only_label: str = "",
|
||||
no_hold_pins: frozenset[int] = frozenset(),
|
||||
) -> None:
|
||||
self.max_pin = max_pin
|
||||
self.strapping_pins = strapping_pins
|
||||
self.usb_jtag_pins = usb_jtag_pins
|
||||
self.error_pins = error_pins
|
||||
self.fatal_pins = fatal_pins
|
||||
self.warning_pins = warning_pins
|
||||
self.input_only_pins = input_only_pins
|
||||
self.input_only_label = input_only_label
|
||||
self.no_hold_pins = no_hold_pins
|
||||
|
||||
def _check_pin_exists(self, num: int) -> None:
|
||||
if num < 0 or num > self.max_pin:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-{self.max_pin})")
|
||||
if (message := self.fatal_pins.get(num)) is not None:
|
||||
raise cv.Invalid(message)
|
||||
|
||||
def validate_gpio_pin(self, value: int) -> int:
|
||||
self._check_pin_exists(value)
|
||||
if (message := self.error_pins.get(value)) is not None:
|
||||
raise cv.Invalid(message)
|
||||
if (message := self.warning_pins.get(value)) is not None:
|
||||
_LOGGER.warning("%s", message)
|
||||
if value in self.usb_jtag_pins:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is used by the USB-Serial-JTAG interface."
|
||||
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
|
||||
value,
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_supports(self, value: ConfigType) -> ConfigType:
|
||||
num = value[CONF_NUMBER]
|
||||
self._check_pin_exists(num)
|
||||
hold = value.get(CONF_HOLD_STATE)
|
||||
if num in self.input_only_pins:
|
||||
prefix = f"GPIO{num}{self.input_only_label}"
|
||||
mode = value[CONF_MODE]
|
||||
for key, problem in _INPUT_ONLY_REJECTED_MODES:
|
||||
if mode[key]:
|
||||
raise cv.Invalid(f"{prefix} {problem}", [CONF_MODE, key])
|
||||
if hold:
|
||||
raise cv.Invalid(
|
||||
f"{prefix} is input-only and cannot be held.", [CONF_HOLD_STATE]
|
||||
)
|
||||
if hold and num in self.no_hold_pins:
|
||||
raise cv.Invalid(f"GPIO{num} has no hold function.", [CONF_HOLD_STATE])
|
||||
if hold and num in self.usb_jtag_pins:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d cannot hold at low level during wakeup from deep sleep.", num
|
||||
)
|
||||
check_strapping_pin(value, self.strapping_pins, _LOGGER)
|
||||
return value
|
||||
@@ -1,17 +1,4 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_INPUT,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_OUTPUT,
|
||||
CONF_PULLDOWN,
|
||||
CONF_PULLUP,
|
||||
)
|
||||
from esphome.pins import check_strapping_pin
|
||||
from .gpio_common import ESP32PinSpec, unusable_errors
|
||||
|
||||
_ESP_SDIO_PINS = {
|
||||
6: "Flash Clock",
|
||||
@@ -20,66 +7,28 @@ _ESP_SDIO_PINS = {
|
||||
11: "Flash Command",
|
||||
}
|
||||
|
||||
_ESP32_STRAPPING_PINS = {0, 2, 5, 12, 15}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 39:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)")
|
||||
if value in _ESP_SDIO_PINS:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32s and is already used by the flash interface"
|
||||
f" (function: {_ESP_SDIO_PINS[value]})."
|
||||
f" If you are using an ESP32 module that uses a different flash pin"
|
||||
f" configuration (e.g. ESP32-PICO-V3-02), you can set"
|
||||
f" 'ignore_pin_validation_error: true' to bypass this check."
|
||||
)
|
||||
if 9 <= value <= 10:
|
||||
_LOGGER.warning(
|
||||
"Pin %s (9-10) might already be used by the flash interface in QUAD IO flash mode.",
|
||||
value,
|
||||
)
|
||||
if value in (24, 28, 29, 30, 31):
|
||||
ESP32_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=39,
|
||||
strapping_pins=frozenset({0, 2, 5, 12, 15}),
|
||||
error_pins={
|
||||
**{
|
||||
num: "This pin cannot be used on ESP32s and is already used by the flash interface"
|
||||
f" (function: {function})."
|
||||
" If you are using an ESP32 module that uses a different flash pin"
|
||||
" configuration (e.g. ESP32-PICO-V3-02), you can set"
|
||||
" 'ignore_pin_validation_error: true' to bypass this check."
|
||||
for num, function in _ESP_SDIO_PINS.items()
|
||||
},
|
||||
# These pins are not exposed in GPIO mux (reason unknown)
|
||||
# but they're missing from IO_MUX list in datasheet
|
||||
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.")
|
||||
return value
|
||||
|
||||
|
||||
def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
is_output = mode[CONF_OUTPUT]
|
||||
is_pullup = mode[CONF_PULLUP]
|
||||
is_pulldown = mode[CONF_PULLDOWN]
|
||||
is_hold = value.get(CONF_HOLD_STATE)
|
||||
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
if is_output and 34 <= num <= 39:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} (34-39) does not support output pin mode.",
|
||||
[CONF_MODE, CONF_OUTPUT],
|
||||
)
|
||||
if is_pullup and 34 <= num <= 39:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} (34-39) does not support pullups.", [CONF_MODE, CONF_PULLUP]
|
||||
)
|
||||
if is_pulldown and 34 <= num <= 39:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN]
|
||||
)
|
||||
if is_hold and 34 <= num <= 39:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} (34-39) is input-only and cannot be held.",
|
||||
[CONF_HOLD_STATE],
|
||||
)
|
||||
if is_hold and num == 20:
|
||||
# Not in the digital hold mask and not an RTC pad, so the driver cannot hold it
|
||||
raise cv.Invalid("GPIO20 has no hold function.", [CONF_HOLD_STATE])
|
||||
|
||||
check_strapping_pin(value, _ESP32_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
**unusable_errors("ESP32", (24, 28, 29, 30, 31)),
|
||||
},
|
||||
warning_pins={
|
||||
num: f"Pin {num} (9-10) might already be used by the flash interface in QUAD IO flash mode."
|
||||
for num in (9, 10)
|
||||
},
|
||||
input_only_pins=frozenset(range(34, 40)),
|
||||
input_only_label=" (34-39)",
|
||||
# Not in the digital hold mask and not an RTC pad, so the driver cannot hold it
|
||||
no_hold_pins=frozenset({20}),
|
||||
)
|
||||
@@ -1,33 +1,3 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
from .gpio_common import ESP32PinSpec
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
_ESP32C2_STRAPPING_PINS = {8, 9}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_c2_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 20:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def esp32_c2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 20:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-20)")
|
||||
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_strapping_pin(value, _ESP32C2_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_C2_PIN_SPEC = ESP32PinSpec(max_pin=20, strapping_pins=frozenset({8, 9}))
|
||||
@@ -1,52 +1,18 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
from .gpio_common import ESP32PinSpec, spi_psram_errors
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
_ESP32C3_SPI_PSRAM_PINS = {
|
||||
12: "SPIHD",
|
||||
13: "SPIWP",
|
||||
14: "SPICS0",
|
||||
15: "SPICLK",
|
||||
16: "SPID",
|
||||
17: "SPIQ",
|
||||
}
|
||||
|
||||
_ESP32C3_USB_JTAG_PINS = {18, 19}
|
||||
|
||||
_ESP32C3_STRAPPING_PINS = {2, 8, 9}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_c3_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 21:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)")
|
||||
if value in _ESP32C3_SPI_PSRAM_PINS:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32C3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 21:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-21)")
|
||||
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32C3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C3_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_C3_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=21,
|
||||
strapping_pins=frozenset({2, 8, 9}),
|
||||
usb_jtag_pins=frozenset({18, 19}),
|
||||
error_pins=spi_psram_errors(
|
||||
"ESP32-C3",
|
||||
{
|
||||
12: "SPIHD",
|
||||
13: "SPIWP",
|
||||
14: "SPICS0",
|
||||
15: "SPICLK",
|
||||
16: "SPID",
|
||||
17: "SPIQ",
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -1,59 +1,28 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
from esphome.const import CONF_SCL, CONF_SDA
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
from .gpio_common import ESP32PinSpec, spi_psram_errors
|
||||
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c5/include/hal/i2c_ll.h
|
||||
_ESP32C5_I2C_LP_PINS = {"SDA": 2, "SCL": 3}
|
||||
|
||||
_ESP32C5_SPI_PSRAM_PINS = {
|
||||
16: "SPICS0",
|
||||
17: "SPIQ",
|
||||
18: "SPIWP",
|
||||
19: "VDD_SPI",
|
||||
20: "SPIHD",
|
||||
21: "SPICLK",
|
||||
22: "SPID",
|
||||
}
|
||||
|
||||
_ESP32C5_STRAPPING_PINS = {2, 7, 27, 28}
|
||||
|
||||
_ESP32C5_USB_JTAG_PINS = {13, 14}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_c5_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 28:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-28)")
|
||||
if value in _ESP32C5_SPI_PSRAM_PINS:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32-C5s and is already used by the SPI/PSRAM interface (function: {_ESP32C5_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
|
||||
check_usb_jtag_pin(value, _ESP32C5_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 28:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-28)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32C5_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_C5_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=28,
|
||||
strapping_pins=frozenset({2, 7, 27, 28}),
|
||||
usb_jtag_pins=frozenset({13, 14}),
|
||||
error_pins=spi_psram_errors(
|
||||
"ESP32-C5",
|
||||
{
|
||||
16: "SPICS0",
|
||||
17: "SPIQ",
|
||||
18: "SPIWP",
|
||||
19: "VDD_SPI",
|
||||
20: "SPIHD",
|
||||
21: "SPICLK",
|
||||
22: "SPID",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def esp32_c5_validate_lp_i2c(value):
|
||||
|
||||
@@ -1,58 +1,28 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
from esphome.const import CONF_SCL, CONF_SDA
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
from .gpio_common import ESP32PinSpec, spi_psram_errors
|
||||
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c6/include/hal/i2c_ll.h
|
||||
_ESP32C6_I2C_LP_PINS = {"SDA": 6, "SCL": 7}
|
||||
|
||||
_ESP32C6_SPI_PSRAM_PINS = {
|
||||
24: "SPICS0",
|
||||
25: "SPIQ",
|
||||
26: "SPIWP",
|
||||
27: "VDD_SPI",
|
||||
28: "SPIHD",
|
||||
29: "SPICLK",
|
||||
30: "SPID",
|
||||
}
|
||||
|
||||
_ESP32C6_USB_JTAG_PINS = {12, 13}
|
||||
|
||||
_ESP32C6_STRAPPING_PINS = {4, 5, 8, 9, 15}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_c6_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 30:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-30)")
|
||||
if value in _ESP32C6_SPI_PSRAM_PINS:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32C6_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 30:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-30)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32C6_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_C6_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=30,
|
||||
strapping_pins=frozenset({4, 5, 8, 9, 15}),
|
||||
usb_jtag_pins=frozenset({12, 13}),
|
||||
error_pins=spi_psram_errors(
|
||||
"ESP32-C6",
|
||||
{
|
||||
24: "SPICS0",
|
||||
25: "SPIQ",
|
||||
26: "SPIWP",
|
||||
27: "VDD_SPI",
|
||||
28: "SPIHD",
|
||||
29: "SPICLK",
|
||||
30: "SPID",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def esp32_c6_validate_lp_i2c(value):
|
||||
|
||||
@@ -1,54 +1,20 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
from .gpio_common import ESP32PinSpec, spi_psram_errors
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM
|
||||
_ESP32C61_SPI_PSRAM_PINS = {
|
||||
14: "SPICS0",
|
||||
15: "SPICLK",
|
||||
16: "SPID",
|
||||
17: "SPIQ",
|
||||
19: "SPIWP",
|
||||
20: "SPIHD",
|
||||
21: "VDD_SPI",
|
||||
}
|
||||
|
||||
_ESP32C61_STRAPPING_PINS = {8, 9}
|
||||
|
||||
_ESP32C61_USB_JTAG_PINS = {12, 13}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_c61_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 29:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-29)")
|
||||
if value in _ESP32C61_SPI_PSRAM_PINS:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32-C61s and is already used by the SPI/PSRAM interface (function: {_ESP32C61_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
|
||||
check_usb_jtag_pin(value, _ESP32C61_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 29:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-29)")
|
||||
if is_input:
|
||||
# All ESP32-C61 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32C61_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32C61_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_C61_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=29,
|
||||
strapping_pins=frozenset({8, 9}),
|
||||
usb_jtag_pins=frozenset({12, 13}),
|
||||
# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM
|
||||
error_pins=spi_psram_errors(
|
||||
"ESP32-C61",
|
||||
{
|
||||
14: "SPICS0",
|
||||
15: "SPICLK",
|
||||
16: "SPID",
|
||||
17: "SPIQ",
|
||||
19: "SPIWP",
|
||||
20: "SPIHD",
|
||||
21: "VDD_SPI",
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -1,46 +1,13 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
from .gpio_common import ESP32PinSpec
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
_ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21}
|
||||
|
||||
_ESP32H2_USB_JTAG_PINS = {26, 27}
|
||||
|
||||
_ESP32H2_STRAPPING_PINS = {8, 9, 25}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_h2_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 27:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)")
|
||||
if value in _ESP32H2_SPI_FLASH_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n"
|
||||
"Utilizing SPI-reserved pins could cause unexpected failures.\n"
|
||||
"See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html",
|
||||
value,
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32H2_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 27:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-27)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32H2_USB_JTAG_PINS, _LOGGER)
|
||||
check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_H2_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=27,
|
||||
strapping_pins=frozenset({8, 9, 25}),
|
||||
usb_jtag_pins=frozenset({26, 27}),
|
||||
warning_pins={
|
||||
num: f"GPIO{num} is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n"
|
||||
"Utilizing SPI-reserved pins could cause unexpected failures.\n"
|
||||
"See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html"
|
||||
for num in (6, 7, 15, 16, 17, 18, 19, 20, 21)
|
||||
},
|
||||
)
|
||||
@@ -1,41 +1,10 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
from .gpio_common import ESP32PinSpec
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# Partial set from the ESP-IDF / esptool boot-mode docs:
|
||||
# https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html
|
||||
# The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section.
|
||||
_ESP32H21_STRAPPING_PINS: set[int] = {13, 14}
|
||||
|
||||
_ESP32H21_USB_JTAG_PINS = {17, 18}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_h21_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 25:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-25)")
|
||||
check_usb_jtag_pin(value, _ESP32H21_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
def esp32_h21_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 25:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-25)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32H21_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32H21_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_H21_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=25,
|
||||
# Partial set from the ESP-IDF / esptool boot-mode docs:
|
||||
# https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html
|
||||
# The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section.
|
||||
strapping_pins=frozenset({13, 14}),
|
||||
usb_jtag_pins=frozenset({17, 18}),
|
||||
)
|
||||
@@ -1,41 +1,10 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
from .gpio_common import ESP32PinSpec
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
# Partial set from the ESP-IDF / esptool boot-mode docs:
|
||||
# https://docs.espressif.com/projects/esptool/en/latest/esp32h4/advanced-topics/boot-mode-selection.html
|
||||
# The full list awaits the ESP32-H4 datasheet's "Strapping Pins" section.
|
||||
_ESP32H4_STRAPPING_PINS: set[int] = {13, 14}
|
||||
|
||||
_ESP32H4_USB_JTAG_PINS = {13, 14}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_h4_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 39:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)")
|
||||
check_usb_jtag_pin(value, _ESP32H4_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
def esp32_h4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 39:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-39)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32H4_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32H4_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_H4_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=39,
|
||||
# Partial set from the ESP-IDF / esptool boot-mode docs:
|
||||
# https://docs.espressif.com/projects/esptool/en/latest/esp32h4/advanced-topics/boot-mode-selection.html
|
||||
# The full list awaits the ESP32-H4 datasheet's "Strapping Pins" section.
|
||||
strapping_pins=frozenset({13, 14}),
|
||||
usb_jtag_pins=frozenset({13, 14}),
|
||||
)
|
||||
@@ -1,43 +1,16 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
from esphome.const import CONF_SCL, CONF_SDA
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
from .gpio_common import ESP32PinSpec
|
||||
|
||||
# https://documentation.espressif.com/esp32-p4-chip-revision-v1.3_datasheet_en.pdf
|
||||
_ESP32P4_LP_PINS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
|
||||
|
||||
_ESP32P4_USB_JTAG_PINS = {24, 25}
|
||||
|
||||
_ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_p4_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 54:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
|
||||
check_usb_jtag_pin(value, _ESP32P4_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def esp32_p4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 54:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-54)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32P4_USB_JTAG_PINS, _LOGGER)
|
||||
check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_P4_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=54,
|
||||
strapping_pins=frozenset({34, 35, 36, 37, 38}),
|
||||
usb_jtag_pins=frozenset({24, 25}),
|
||||
)
|
||||
|
||||
|
||||
def esp32_p4_validate_lp_i2c(value):
|
||||
|
||||
@@ -1,81 +1,30 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_INPUT,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_OUTPUT,
|
||||
CONF_PULLDOWN,
|
||||
CONF_PULLUP,
|
||||
)
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
_ESP32S2_SPI_PSRAM_PINS = {
|
||||
26: "SPICS1",
|
||||
27: "SPIHD",
|
||||
28: "SPIWP",
|
||||
29: "SPICS0",
|
||||
30: "SPICLK",
|
||||
31: "SPIQ",
|
||||
32: "SPID",
|
||||
}
|
||||
|
||||
_ESP32S2_STRAPPING_PINS = {0, 45, 46}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .gpio_common import ESP32PinSpec, spi_psram_errors, unusable_errors
|
||||
|
||||
|
||||
def esp32_s2_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 46:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)")
|
||||
|
||||
if value in _ESP32S2_SPI_PSRAM_PINS:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32-S2s and is already used by the SPI/PSRAM interface (function: {_ESP32S2_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
|
||||
if value in (22, 23, 24, 25):
|
||||
def s2_s3_error_pins(variant: str) -> dict[int, str]:
|
||||
"""Errors for the flash/PSRAM and missing pins, which S2 and S3 share."""
|
||||
return {
|
||||
**spi_psram_errors(
|
||||
variant,
|
||||
{
|
||||
26: "SPICS1",
|
||||
27: "SPIHD",
|
||||
28: "SPIWP",
|
||||
29: "SPICS0",
|
||||
30: "SPICLK",
|
||||
31: "SPIQ",
|
||||
32: "SPID",
|
||||
},
|
||||
),
|
||||
# These pins are not exposed in GPIO mux (reason unknown)
|
||||
# but they're missing from IO_MUX list in datasheet
|
||||
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S2s.")
|
||||
|
||||
return value
|
||||
**unusable_errors(variant, (22, 23, 24, 25)),
|
||||
}
|
||||
|
||||
|
||||
def esp32_s2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
is_output = mode[CONF_OUTPUT]
|
||||
is_pullup = mode[CONF_PULLUP]
|
||||
is_pulldown = mode[CONF_PULLDOWN]
|
||||
|
||||
if num < 0 or num > 46:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
if is_output and num == 46:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} does not support output pin mode.",
|
||||
[CONF_MODE, CONF_OUTPUT],
|
||||
)
|
||||
if is_pullup and num == 46:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} does not support pullups.", [CONF_MODE, CONF_PULLUP]
|
||||
)
|
||||
if is_pulldown and num == 46:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN]
|
||||
)
|
||||
if value.get(CONF_HOLD_STATE) and num == 46:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{num} is input-only and cannot be held.",
|
||||
[CONF_HOLD_STATE],
|
||||
)
|
||||
|
||||
check_strapping_pin(value, _ESP32S2_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_S2_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=46,
|
||||
strapping_pins=frozenset({0, 45, 46}),
|
||||
error_pins=s2_s3_error_pins("ESP32-S2"),
|
||||
input_only_pins=frozenset({46}),
|
||||
)
|
||||
@@ -1,28 +1,11 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DISABLED,
|
||||
CONF_INPUT,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
PLATFORM_ESP32,
|
||||
)
|
||||
from esphome.pins import PIN_SCHEMA_REGISTRY, check_strapping_pin
|
||||
from esphome.const import CONF_DISABLED, CONF_MODE, PLATFORM_ESP32
|
||||
from esphome.pins import PIN_SCHEMA_REGISTRY
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
|
||||
_ESP32S3_SPI_PSRAM_PINS = {
|
||||
26: "SPICS1",
|
||||
27: "SPIHD",
|
||||
28: "SPIWP",
|
||||
29: "SPICS0",
|
||||
30: "SPICLK",
|
||||
31: "SPIQ",
|
||||
32: "SPID",
|
||||
}
|
||||
from .gpio_common import ESP32PinSpec
|
||||
from .gpio_esp32_s2 import s2_s3_error_pins
|
||||
|
||||
_ESP32S3R8_PSRAM_PINS = {
|
||||
33: "SPIIO4",
|
||||
@@ -32,48 +15,14 @@ _ESP32S3R8_PSRAM_PINS = {
|
||||
37: "SPIDQS",
|
||||
}
|
||||
|
||||
_ESP32S3_USB_JTAG_PINS = {19, 20}
|
||||
|
||||
_ESP32S3_STRAPPING_PINS = {0, 3, 45, 46}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_s3_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 48:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)")
|
||||
|
||||
if value in _ESP32S3_SPI_PSRAM_PINS:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP32S3_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
# GPIO33-37 (_ESP32S3R8_PSRAM_PINS) are only taken by the PSRAM interface in
|
||||
# octal mode -- whether that applies isn't known here, so the warning is
|
||||
# deferred to final_validate_pins() in gpio.py once the PSRAM mode is resolved.
|
||||
|
||||
if value in (22, 23, 24, 25):
|
||||
# These pins are not exposed in GPIO mux (reason unknown)
|
||||
# but they're missing from IO_MUX list in datasheet
|
||||
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.")
|
||||
check_usb_jtag_pin(value, _ESP32S3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 48:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-48)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_usb_jtag_hold(value, _ESP32S3_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_S3_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=48,
|
||||
strapping_pins=frozenset({0, 3, 45, 46}),
|
||||
usb_jtag_pins=frozenset({19, 20}),
|
||||
error_pins=s2_s3_error_pins("ESP32-S3"),
|
||||
)
|
||||
|
||||
|
||||
def esp32_s3_final_validate_pins(full_config: ConfigType) -> None:
|
||||
|
||||
@@ -1,60 +1,27 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
from esphome.const import CONF_SCL, CONF_SDA
|
||||
|
||||
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
|
||||
from .gpio_common import ESP32PinSpec
|
||||
|
||||
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
|
||||
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
|
||||
|
||||
# Per the ESP32-S31 IDF DOCS and datasheet:
|
||||
# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html
|
||||
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
|
||||
_ESP32S31_SPI_FLASH_PINS: set[int] = {26, 27, 28, 30, 31, 32}
|
||||
_ESP32S31_INVALID_PINS: set[int] = {29, 41}
|
||||
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source;
|
||||
# GPIO36 sets the VDD_SPI voltage.
|
||||
_ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61}
|
||||
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
|
||||
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
|
||||
|
||||
_ESP32S31_USB_JTAG_PINS = {33, 34}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_s31_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 61:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)")
|
||||
if value in _ESP32S31_INVALID_PINS:
|
||||
raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.")
|
||||
if value in _ESP32S31_SPI_FLASH_PINS:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
|
||||
)
|
||||
check_usb_jtag_pin(value, _ESP32S31_USB_JTAG_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 61:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)")
|
||||
# Checked here as well so ignore_pin_validation_error cannot bypass it;
|
||||
# these pins are not bonded and can never work
|
||||
if num in _ESP32S31_INVALID_PINS:
|
||||
raise cv.Invalid(f"GPIO{num} does not exist on ESP32-S31.")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_usb_jtag_hold(value, _ESP32S31_USB_JTAG_PINS, _LOGGER)
|
||||
|
||||
check_strapping_pin(value, _ESP32S31_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
ESP32_S31_PIN_SPEC = ESP32PinSpec(
|
||||
max_pin=61,
|
||||
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source;
|
||||
# GPIO36 sets the VDD_SPI voltage.
|
||||
strapping_pins=frozenset({36, 37, 60, 61}),
|
||||
usb_jtag_pins=frozenset({33, 34}),
|
||||
error_pins={
|
||||
num: f"GPIO{num} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
|
||||
for num in (26, 27, 28, 30, 31, 32)
|
||||
},
|
||||
# Not bonded, so they can never work
|
||||
fatal_pins={num: f"GPIO{num} does not exist on ESP32-S31." for num in (29, 41)},
|
||||
)
|
||||
|
||||
|
||||
def esp32_s31_validate_lp_i2c(value):
|
||||
|
||||
@@ -38,7 +38,7 @@ void ESP32ULP::setup() {
|
||||
esp_err_t err = ulp_set_wakeup_period(0, this->wakeup_period_us_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ulp_set_wakeup_period failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Wakeup period not set"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ void ESP32ULP::setup() {
|
||||
if (esp_reset_reason() == ESP_RST_DEEPSLEEP && s_program_loaded) {
|
||||
this->loaded_ = true;
|
||||
} else if (this->run_on_boot_ && !this->load_and_run()) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Program did not start"));
|
||||
return;
|
||||
}
|
||||
if (this->wakeup_from_sleep_) {
|
||||
|
||||
@@ -6,6 +6,8 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["factory_reset"]
|
||||
|
||||
CONFIG_SCHEMA = button.button_schema(
|
||||
button.ApplyButton,
|
||||
device_class=DEVICE_CLASS_RESTART,
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
#include "factory_reset_button.h"
|
||||
#include "factory_reset_device.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
namespace esphome::factory_reset {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "factory_reset.button");
|
||||
ESPHOME_LOG_TAG(TAG, "factory_reset");
|
||||
|
||||
static void reset_and_reboot() {
|
||||
global_preferences->reset();
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace esphome::factory_reset {
|
||||
|
||||
/// Run by the factory reset button.
|
||||
/// Run by the factory reset button and switch.
|
||||
void factory_reset_device();
|
||||
|
||||
} // namespace esphome::factory_reset
|
||||
@@ -1,23 +1,21 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import factory_reset_ns
|
||||
|
||||
FactoryResetSwitch = factory_reset_ns.class_(
|
||||
"FactoryResetSwitch", switch.Switch, cg.Component
|
||||
)
|
||||
AUTO_LOAD = ["factory_reset"]
|
||||
|
||||
CONFIG_SCHEMA = switch.switch_schema(
|
||||
FactoryResetSwitch,
|
||||
switch.ApplySwitch,
|
||||
block_inverted=True,
|
||||
icon=ICON_RESTART_ALERT,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
await switch.sub_switches(config).create(
|
||||
config,
|
||||
None,
|
||||
apply="factory_reset::factory_reset_device()",
|
||||
publish="momentary",
|
||||
)
|
||||
@@ -1,41 +0,0 @@
|
||||
#include "factory_reset_switch.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_OPENTHREAD
|
||||
#include "esphome/components/openthread/openthread.h"
|
||||
#endif
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::factory_reset {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "factory_reset.switch");
|
||||
|
||||
void FactoryResetSwitch::dump_config() { LOG_SWITCH("", "Factory Reset Switch", this); }
|
||||
void FactoryResetSwitch::write_state(bool state) {
|
||||
// Acknowledge
|
||||
this->publish_state(false);
|
||||
|
||||
if (state) {
|
||||
ESP_LOGI(TAG, "Resetting");
|
||||
// Let MQTT settle a bit
|
||||
delay(100); // NOLINT
|
||||
#ifdef USE_OPENTHREAD
|
||||
openthread::global_openthread_component->on_factory_reset(FactoryResetSwitch::factory_reset_callback);
|
||||
#else
|
||||
global_preferences->reset();
|
||||
App.safe_reboot();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_OPENTHREAD
|
||||
void FactoryResetSwitch::factory_reset_callback() {
|
||||
global_preferences->reset();
|
||||
App.safe_reboot();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::factory_reset
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome::factory_reset {
|
||||
|
||||
class FactoryResetSwitch final : public switch_::Switch, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
#ifdef USE_OPENTHREAD
|
||||
static void factory_reset_callback();
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::factory_reset
|
||||
@@ -185,6 +185,10 @@ _PRESET_MODES_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
# speed_count_ is a uint16_t in FanTraits
|
||||
validate_speed_count = cv.int_range(min=1, max=65535)
|
||||
|
||||
|
||||
def validate_preset_modes(value):
|
||||
# Check against defined schema
|
||||
value = _PRESET_MODES_SCHEMA(value)
|
||||
|
||||
@@ -17,24 +17,24 @@ class FanTraits {
|
||||
public:
|
||||
FanTraits() = default;
|
||||
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
|
||||
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
|
||||
: flags_{oscillation, speed, direction, 0}, speed_count_(speed_count) {}
|
||||
|
||||
/// Return if this fan supports oscillation.
|
||||
bool supports_oscillation() const { return this->oscillation_; }
|
||||
bool supports_oscillation() const { return this->flags_.oscillation; }
|
||||
/// Set whether this fan supports oscillation.
|
||||
void set_oscillation(bool oscillation) { this->oscillation_ = oscillation; }
|
||||
void set_oscillation(bool oscillation) { this->flags_.oscillation = oscillation; }
|
||||
/// Return if this fan supports speed modes.
|
||||
bool supports_speed() const { return this->speed_; }
|
||||
bool supports_speed() const { return this->flags_.speed; }
|
||||
/// Set whether this fan supports speed levels.
|
||||
void set_speed(bool speed) { this->speed_ = speed; }
|
||||
void set_speed(bool speed) { this->flags_.speed = speed; }
|
||||
/// Return how many speed levels the fan has
|
||||
int supported_speed_count() const { return this->speed_count_; }
|
||||
/// Set how many speed levels this fan has.
|
||||
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
|
||||
/// Return if this fan supports changing direction
|
||||
bool supports_direction() const { return this->direction_; }
|
||||
bool supports_direction() const { return this->flags_.direction; }
|
||||
/// Set whether this fan supports changing direction
|
||||
void set_direction(bool direction) { this->direction_ = direction; }
|
||||
void set_direction(bool direction) { this->flags_.direction = direction; }
|
||||
/// Empty when the fan has no preset modes. Set them on the Fan entity.
|
||||
const FanPresetModes &supported_preset_modes() const;
|
||||
|
||||
@@ -60,10 +60,14 @@ class FanTraits {
|
||||
/// Set the preset modes pointer (only Fan::wire_preset_modes_() should call this).
|
||||
void set_supported_preset_modes_(const FanPresetModes *preset_modes) { this->preset_modes_ = preset_modes; }
|
||||
|
||||
bool oscillation_{false};
|
||||
bool speed_{false};
|
||||
bool direction_{false};
|
||||
int speed_count_{};
|
||||
// Bit-packed flags to save memory (1 byte instead of 3)
|
||||
struct FanTraitFlags {
|
||||
uint8_t oscillation : 1;
|
||||
uint8_t speed : 1;
|
||||
uint8_t direction : 1;
|
||||
uint8_t reserved : 5;
|
||||
} flags_{};
|
||||
uint16_t speed_count_{}; // validators cap configs at 65535
|
||||
const FanPresetModes *preset_modes_{nullptr}; // owned by the Fan entity
|
||||
};
|
||||
static_assert(std::is_trivially_copyable_v<FanTraits>, "FanTraits is returned by value from get_traits()");
|
||||
|
||||
@@ -286,9 +286,7 @@ bool FUSB302B::reset_pd_() {
|
||||
}
|
||||
|
||||
bool FUSB302B::enable_auto_crc_() {
|
||||
uint8_t switches1 = 0;
|
||||
return this->read_byte(FUSB_SWITCHES1, &switches1) &&
|
||||
this->write_byte(FUSB_SWITCHES1, switches1 | FUSB_SWITCHES1_AUTO_CRC);
|
||||
return this->update_register(FUSB_SWITCHES1, FUSB_SWITCHES1_AUTO_CRC, FUSB_SWITCHES1_AUTO_CRC);
|
||||
}
|
||||
|
||||
void FUSB302B::handle_interrupt_() {
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, output
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_ID
|
||||
from esphome.const import CONF_CHANNEL
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_GP8403_ID, GP8403Component, gp8403_ns
|
||||
from .. import CONF_GP8403_ID, GP8403Component
|
||||
|
||||
DEPENDENCIES = ["gp8403"]
|
||||
|
||||
GP8403Output = gp8403_ns.class_(
|
||||
"GP8403Output", cg.Component, i2c.I2CDevice, output.FloatOutput
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(GP8403Output),
|
||||
cv.GenerateID(): cv.declare_id(output.ApplyFloatOutput),
|
||||
cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403Component),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
|
||||
@@ -27,10 +23,6 @@ FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await output.register_output(var, config)
|
||||
|
||||
await cg.register_parented(var, config[CONF_GP8403_ID])
|
||||
|
||||
cg.add(var.set_channel(config[CONF_CHANNEL]))
|
||||
await output.sub_float_outputs(config, parent=config[CONF_GP8403_ID]).create(
|
||||
config, None, config[CONF_CHANNEL], apply="write_state(state, index)"
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
#include "gp8403_output.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::gp8403 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "gp8403.output");
|
||||
|
||||
void GP8403Output::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"GP8403 Output:\n"
|
||||
" Channel: %u",
|
||||
this->channel_);
|
||||
}
|
||||
|
||||
void GP8403Output::write_state(float state) { this->parent_->write_state(state, this->channel_); }
|
||||
|
||||
} // namespace esphome::gp8403
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/output/float_output.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include "esphome/components/gp8403/gp8403.h"
|
||||
|
||||
namespace esphome::gp8403 {
|
||||
|
||||
class GP8403Output final : public Component, public output::FloatOutput, public Parented<GP8403Component> {
|
||||
public:
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA - 1; }
|
||||
void set_channel(uint8_t channel) { this->channel_ = channel; }
|
||||
void write_state(float state) override;
|
||||
|
||||
protected:
|
||||
uint8_t channel_;
|
||||
};
|
||||
|
||||
} // namespace esphome::gp8403
|
||||
@@ -5,12 +5,7 @@ namespace esphome::gree {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "gree.switch");
|
||||
|
||||
void GreeModeBitSwitch::setup() {
|
||||
auto initial = this->get_initial_state_with_restore_mode();
|
||||
if (initial.has_value()) {
|
||||
this->write_state(*initial);
|
||||
}
|
||||
}
|
||||
void GreeModeBitSwitch::setup() { this->restore(); }
|
||||
|
||||
void GreeModeBitSwitch::dump_config() { log_switch(TAG, " ", this->name_, this); }
|
||||
|
||||
|
||||
@@ -3,20 +3,11 @@ from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BEEPER, CONF_DISPLAY, ENTITY_CATEGORY_CONFIG
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..climate import (
|
||||
CONF_HAIER_ID,
|
||||
CONF_PROTOCOL,
|
||||
PROTOCOL_HON,
|
||||
HaierClimateBase,
|
||||
haier_ns,
|
||||
)
|
||||
from ..climate import CONF_HAIER_ID, CONF_PROTOCOL, PROTOCOL_HON, HaierClimateBase
|
||||
|
||||
CODEOWNERS = ["@paveldn"]
|
||||
BeeperSwitch = haier_ns.class_("BeeperSwitch", switch.Switch)
|
||||
HealthModeSwitch = haier_ns.class_("HealthModeSwitch", switch.Switch)
|
||||
DisplaySwitch = haier_ns.class_("DisplaySwitch", switch.Switch)
|
||||
QuietModeSwitch = haier_ns.class_("QuietModeSwitch", switch.Switch)
|
||||
|
||||
# Haier switches
|
||||
CONF_HEALTH_MODE = "health_mode"
|
||||
@@ -32,26 +23,26 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HaierClimateBase),
|
||||
cv.Optional(CONF_DISPLAY): switch.switch_schema(
|
||||
DisplaySwitch,
|
||||
switch.ApplySwitch,
|
||||
icon=ICON_LED_ON,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
default_restore_mode="DISABLED",
|
||||
),
|
||||
cv.Optional(CONF_HEALTH_MODE): switch.switch_schema(
|
||||
HealthModeSwitch,
|
||||
switch.ApplySwitch,
|
||||
icon=ICON_LEAF,
|
||||
default_restore_mode="DISABLED",
|
||||
),
|
||||
# Beeper switch is only supported for HonClimate
|
||||
cv.Optional(CONF_BEEPER): switch.switch_schema(
|
||||
BeeperSwitch,
|
||||
switch.ApplySwitch,
|
||||
icon=ICON_VOLUME_HIGH,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
default_restore_mode="DISABLED",
|
||||
),
|
||||
# Quiet mode is only supported for HonClimate
|
||||
cv.Optional(CONF_QUIET_MODE): switch.switch_schema(
|
||||
QuietModeSwitch,
|
||||
switch.ApplySwitch,
|
||||
icon=ICON_VOLUME_OFF,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
default_restore_mode="DISABLED",
|
||||
@@ -77,11 +68,31 @@ def _final_validate(config) -> None:
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
parent = await cg.get_variable(config[CONF_HAIER_ID])
|
||||
|
||||
for switch_type in [CONF_DISPLAY, CONF_HEALTH_MODE, CONF_BEEPER, CONF_QUIET_MODE]:
|
||||
if conf := config.get(switch_type):
|
||||
sw_var = await switch.new_switch(conf)
|
||||
await cg.register_parented(sw_var, parent)
|
||||
cg.add(getattr(parent, f"set_{switch_type}_switch")(sw_var))
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_HAIER_ID])
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
# The setters skip an unchanged value; the switch reports the state either way
|
||||
await switches(
|
||||
CONF_DISPLAY,
|
||||
hub.set_display_switch,
|
||||
apply="set_display_state(state)",
|
||||
publish="after",
|
||||
)
|
||||
await switches(
|
||||
CONF_HEALTH_MODE,
|
||||
hub.set_health_mode_switch,
|
||||
apply="set_health_mode(state)",
|
||||
publish="after",
|
||||
)
|
||||
await switches(
|
||||
CONF_BEEPER,
|
||||
hub.set_beeper_switch,
|
||||
apply="set_beeper_state(state)",
|
||||
publish="after",
|
||||
)
|
||||
await switches(
|
||||
CONF_QUIET_MODE,
|
||||
hub.set_quiet_mode_switch,
|
||||
apply="set_quiet_mode_state(state)",
|
||||
publish="after",
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
#include "beeper.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
void BeeperSwitch::write_state(bool state) {
|
||||
if (this->parent_->get_beeper_state() != state) {
|
||||
this->parent_->set_beeper_state(state);
|
||||
}
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "../hon_climate.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
class BeeperSwitch final : public switch_::Switch, public Parented<HonClimate> {
|
||||
public:
|
||||
BeeperSwitch() = default;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,12 +0,0 @@
|
||||
#include "display.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
void DisplaySwitch::write_state(bool state) {
|
||||
if (this->parent_->get_display_state() != state) {
|
||||
this->parent_->set_display_state(state);
|
||||
}
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "../haier_base.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
class DisplaySwitch final : public switch_::Switch, public Parented<HaierClimateBase> {
|
||||
public:
|
||||
DisplaySwitch() = default;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,12 +0,0 @@
|
||||
#include "health_mode.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
void HealthModeSwitch::write_state(bool state) {
|
||||
if (this->parent_->get_health_mode() != state) {
|
||||
this->parent_->set_health_mode(state);
|
||||
}
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "../haier_base.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
class HealthModeSwitch final : public switch_::Switch, public Parented<HaierClimateBase> {
|
||||
public:
|
||||
HealthModeSwitch() = default;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,12 +0,0 @@
|
||||
#include "quiet_mode.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
void QuietModeSwitch::write_state(bool state) {
|
||||
if (this->parent_->get_quiet_mode_state() != state) {
|
||||
this->parent_->set_quiet_mode_state(state);
|
||||
}
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "../hon_climate.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
class QuietModeSwitch final : public switch_::Switch, public Parented<HonClimate> {
|
||||
public:
|
||||
QuietModeSwitch() = default;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -8,9 +8,10 @@ ESPHOME_LOG_TAG(TAG, "haier_tundra.switch");
|
||||
void HaierTundraSwitch::setup() {
|
||||
auto initial = this->get_initial_state_with_restore_mode();
|
||||
if (initial.has_value()) {
|
||||
const bool state = initial.value();
|
||||
this->apply_(state, false);
|
||||
this->publish_state(state);
|
||||
// apply_() and publish_state() take the level, which an inverted switch flips
|
||||
const bool level = *initial != this->is_inverted();
|
||||
this->apply_(level, false);
|
||||
this->publish_state(level);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_POWER,
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
DEVICE_CLASS_HEAT,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .water_heater import CONF_HAIER_WATER_HEATER_ID, HonWaterHeater
|
||||
|
||||
CONF_COMMUNICATION = "communication"
|
||||
CONF_HEATING = "heating"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HAIER_WATER_HEATER_ID): cv.use_id(HonWaterHeater),
|
||||
cv.Optional(CONF_COMMUNICATION): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_POWER): binary_sensor.binary_sensor_schema(),
|
||||
cv.Optional(CONF_HEATING): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_HEAT
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_HAIER_WATER_HEATER_ID])
|
||||
binary_sensors = binary_sensor.sub_binary_sensors(config)
|
||||
await binary_sensors(CONF_COMMUNICATION, hub.set_communication_binary_sensor)
|
||||
await binary_sensors(CONF_POWER, hub.set_power_binary_sensor)
|
||||
await binary_sensors(CONF_HEATING, hub.set_heating_binary_sensor)
|
||||
@@ -14,19 +14,27 @@ namespace esphome::haier_water_heater {
|
||||
ESPHOME_LOG_TAG(TAG, "haier_water_heater");
|
||||
using namespace water_heater_protocol;
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
// Indexed by HonMode, with Unknown as the fallback for out-of-range values.
|
||||
PROGMEM_STRING_TABLE(HonModeLabels, "Unknown", "Normal", "ECO", "BPS");
|
||||
#endif
|
||||
|
||||
void HonWaterHeater::setup() {
|
||||
this->status_set_warning(LOG_STR("Waiting for a valid appliance status frame"));
|
||||
if (this->communication_sensor_ != nullptr)
|
||||
this->communication_sensor_->publish_state(false);
|
||||
this->publish_communication_(false);
|
||||
this->update_network_status_();
|
||||
this->set_interval(NETWORK_STATUS_INTERVAL_ID, NETWORK_STATUS_INTERVAL_MS,
|
||||
[this]() { this->update_network_status_(); });
|
||||
this->set_interval(STATUS_POLL_INTERVAL_ID, POLL_INTERVAL_MS, [this]() { this->poll_status_(); });
|
||||
}
|
||||
|
||||
void HonWaterHeater::publish_communication_(bool connected) {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->communication_binary_sensor_ != nullptr)
|
||||
this->communication_binary_sensor_->publish_state(connected);
|
||||
#endif
|
||||
}
|
||||
|
||||
void HonWaterHeater::update_network_status_() {
|
||||
#ifdef USE_NETWORK
|
||||
const bool ready = network::is_connected();
|
||||
@@ -87,8 +95,7 @@ void HonWaterHeater::check_timeouts_(uint32_t now) {
|
||||
this->status_set_warning(LOG_STR("Heater communication timed out"));
|
||||
ESP_LOGW(TAG, "Heater communication lost: no valid status for %u ms",
|
||||
static_cast<unsigned>(this->communication_timeout_));
|
||||
if (this->communication_sensor_ != nullptr)
|
||||
this->communication_sensor_->publish_state(false);
|
||||
this->publish_communication_(false);
|
||||
}
|
||||
const uint32_t waited = now - this->request_sent_;
|
||||
switch (this->awaiting_) {
|
||||
@@ -243,8 +250,7 @@ void HonWaterHeater::process_frame_() {
|
||||
// Published last so an automation on the sensor sees fresh heater state and can send a control.
|
||||
if (recovered) {
|
||||
ESP_LOGI(TAG, "Heater communication established");
|
||||
if (this->communication_sensor_ != nullptr)
|
||||
this->communication_sensor_->publish_state(true);
|
||||
this->publish_communication_(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,33 +273,35 @@ void HonWaterHeater::publish_status_(const Status &status) {
|
||||
}
|
||||
// The base entity has no Unknown mode; the text sensor preserves the decoded result.
|
||||
this->mode_ = status.powered ? this->last_active_mode_ : water_heater::WATER_HEATER_MODE_OFF;
|
||||
if (this->primary_sensor_ != nullptr)
|
||||
this->primary_sensor_->publish_state(status.current_temperature);
|
||||
if (this->secondary_sensor_ != nullptr) {
|
||||
this->secondary_sensor_->publish_state(
|
||||
#ifdef USE_SENSOR
|
||||
if (this->primary_temperature_sensor_ != nullptr)
|
||||
this->primary_temperature_sensor_->publish_state(status.current_temperature);
|
||||
if (this->secondary_temperature_sensor_ != nullptr) {
|
||||
this->secondary_temperature_sensor_->publish_state(
|
||||
status.secondary_temperature <= MAX_REPORTED_TEMPERATURE ? status.secondary_temperature : NAN);
|
||||
}
|
||||
if (this->target_sensor_ != nullptr)
|
||||
this->target_sensor_->publish_state(status.target_temperature);
|
||||
if (this->heating_time_sensor_ != nullptr)
|
||||
this->heating_time_sensor_->publish_state(status.estimated_heating_time);
|
||||
if (this->water_sensor_ != nullptr) {
|
||||
if (this->target_temperature_sensor_ != nullptr)
|
||||
this->target_temperature_sensor_->publish_state(status.target_temperature);
|
||||
if (this->estimated_heating_time_sensor_ != nullptr)
|
||||
this->estimated_heating_time_sensor_->publish_state(status.estimated_heating_time);
|
||||
if (this->available_water_sensor_ != nullptr) {
|
||||
// Three segments of available hot water, published as 0, 33, 66 or 100 percent.
|
||||
const uint8_t percent = status.available_water * 100 / 3;
|
||||
this->water_sensor_->publish_state(status.available_water <= 3 ? percent : NAN);
|
||||
this->available_water_sensor_->publish_state(status.available_water <= 3 ? percent : NAN);
|
||||
}
|
||||
if (this->power_sensor_ != nullptr)
|
||||
this->power_sensor_->publish_state(status.powered);
|
||||
if (this->heating_sensor_ != nullptr)
|
||||
this->heating_sensor_->publish_state(status.heating);
|
||||
if (this->mode_sensor_ != nullptr) {
|
||||
char label[HonModeLabels::MAX_LENGTH + 1];
|
||||
ESPHOME_strncpy_P(label,
|
||||
reinterpret_cast<ESPHOME_PGM_P>(HonModeLabels::get_progmem_str(
|
||||
static_cast<uint8_t>(status.mode), static_cast<uint8_t>(HonMode::HON_MODE_UNKNOWN))),
|
||||
sizeof(label));
|
||||
this->mode_sensor_->publish_state(label);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->power_binary_sensor_ != nullptr)
|
||||
this->power_binary_sensor_->publish_state(status.powered);
|
||||
if (this->heating_binary_sensor_ != nullptr)
|
||||
this->heating_binary_sensor_->publish_state(status.heating);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->mode_text_sensor_ != nullptr) {
|
||||
this->mode_text_sensor_->publish_state(HonModeLabels::get_progmem_str(
|
||||
static_cast<uint8_t>(status.mode), static_cast<uint8_t>(HonMode::HON_MODE_UNKNOWN)));
|
||||
}
|
||||
#endif
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
@@ -303,15 +311,21 @@ void HonWaterHeater::dump_config() {
|
||||
" Polling interval: %u ms\n"
|
||||
" Communication timeout: %u ms",
|
||||
static_cast<unsigned>(POLL_INTERVAL_MS), static_cast<unsigned>(this->communication_timeout_));
|
||||
LOG_SENSOR(" ", "Primary temperature", this->primary_sensor_);
|
||||
LOG_SENSOR(" ", "Secondary temperature", this->secondary_sensor_);
|
||||
LOG_SENSOR(" ", "Target temperature", this->target_sensor_);
|
||||
LOG_SENSOR(" ", "Estimated heating time", this->heating_time_sensor_);
|
||||
LOG_SENSOR(" ", "Available water", this->water_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Communication", this->communication_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Power", this->power_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Heating", this->heating_sensor_);
|
||||
LOG_TEXT_SENSOR(" ", "Mode", this->mode_sensor_);
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR(" ", "Primary temperature", this->primary_temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Secondary temperature", this->secondary_temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Target temperature", this->target_temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Estimated heating time", this->estimated_heating_time_sensor_);
|
||||
LOG_SENSOR(" ", "Available water", this->available_water_sensor_);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Communication", this->communication_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Power", this->power_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Heating", this->heating_binary_sensor_);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
LOG_TEXT_SENSOR(" ", "Mode", this->mode_text_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
water_heater::WaterHeaterCallInternal HonWaterHeater::make_call() {
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
#include "hon_parser.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::haier_water_heater {
|
||||
|
||||
@@ -18,15 +24,21 @@ class HonWaterHeater : public Component, public water_heater::WaterHeater, publi
|
||||
water_heater::WaterHeaterCallInternal make_call() override;
|
||||
|
||||
void set_communication_timeout(uint32_t value) { this->communication_timeout_ = value; }
|
||||
void set_primary_temperature_sensor(sensor::Sensor *value) { this->primary_sensor_ = value; }
|
||||
void set_secondary_temperature_sensor(sensor::Sensor *value) { this->secondary_sensor_ = value; }
|
||||
void set_target_temperature_sensor(sensor::Sensor *value) { this->target_sensor_ = value; }
|
||||
void set_estimated_heating_time_sensor(sensor::Sensor *value) { this->heating_time_sensor_ = value; }
|
||||
void set_available_water_sensor(sensor::Sensor *value) { this->water_sensor_ = value; }
|
||||
void set_communication_sensor(binary_sensor::BinarySensor *value) { this->communication_sensor_ = value; }
|
||||
void set_power_sensor(binary_sensor::BinarySensor *value) { this->power_sensor_ = value; }
|
||||
void set_heating_sensor(binary_sensor::BinarySensor *value) { this->heating_sensor_ = value; }
|
||||
void set_mode_sensor(text_sensor::TextSensor *value) { this->mode_sensor_ = value; }
|
||||
#ifdef USE_SENSOR
|
||||
SUB_SENSOR(primary_temperature)
|
||||
SUB_SENSOR(secondary_temperature)
|
||||
SUB_SENSOR(target_temperature)
|
||||
SUB_SENSOR(estimated_heating_time)
|
||||
SUB_SENSOR(available_water)
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
SUB_BINARY_SENSOR(communication)
|
||||
SUB_BINARY_SENSOR(power)
|
||||
SUB_BINARY_SENSOR(heating)
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
SUB_TEXT_SENSOR(mode)
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static constexpr uint32_t NETWORK_STATUS_INTERVAL_ID = 0;
|
||||
@@ -55,6 +67,7 @@ class HonWaterHeater : public Component, public water_heater::WaterHeater, publi
|
||||
void read_frames_(uint32_t now);
|
||||
void poll_status_();
|
||||
void update_network_status_();
|
||||
void publish_communication_(bool connected);
|
||||
void send_frame_(water_heater_protocol::FrameType type, const uint8_t *payload = nullptr, size_t size = 0);
|
||||
void send_request_(uint32_t now, Awaiting kind, water_heater_protocol::FrameType type, const uint8_t *payload,
|
||||
size_t size, water_heater_protocol::FrameType expected);
|
||||
@@ -65,15 +78,6 @@ class HonWaterHeater : public Component, public water_heater::WaterHeater, publi
|
||||
void publish_status_(const water_heater_protocol::Status &status);
|
||||
|
||||
water_heater_protocol::FrameParser parser_;
|
||||
sensor::Sensor *primary_sensor_{nullptr};
|
||||
sensor::Sensor *secondary_sensor_{nullptr};
|
||||
sensor::Sensor *target_sensor_{nullptr};
|
||||
sensor::Sensor *heating_time_sensor_{nullptr};
|
||||
sensor::Sensor *water_sensor_{nullptr};
|
||||
binary_sensor::BinarySensor *communication_sensor_{nullptr};
|
||||
binary_sensor::BinarySensor *power_sensor_{nullptr};
|
||||
binary_sensor::BinarySensor *heating_sensor_{nullptr};
|
||||
text_sensor::TextSensor *mode_sensor_{nullptr};
|
||||
uint32_t request_sent_{0};
|
||||
uint32_t network_sent_{0};
|
||||
uint32_t last_status_{0};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_MINUTE,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .water_heater import CONF_HAIER_WATER_HEATER_ID, HonWaterHeater
|
||||
|
||||
CONF_AVAILABLE_WATER = "available_water"
|
||||
CONF_ESTIMATED_HEATING_TIME = "estimated_heating_time"
|
||||
CONF_PRIMARY_TEMPERATURE = "primary_temperature"
|
||||
CONF_SECONDARY_TEMPERATURE = "secondary_temperature"
|
||||
|
||||
_TEMPERATURE_SENSOR = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HAIER_WATER_HEATER_ID): cv.use_id(HonWaterHeater),
|
||||
cv.Optional(CONF_PRIMARY_TEMPERATURE): _TEMPERATURE_SENSOR,
|
||||
cv.Optional(CONF_SECONDARY_TEMPERATURE): _TEMPERATURE_SENSOR,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): _TEMPERATURE_SENSOR,
|
||||
cv.Optional(CONF_ESTIMATED_HEATING_TIME): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MINUTE,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_AVAILABLE_WATER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_HAIER_WATER_HEATER_ID])
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_PRIMARY_TEMPERATURE, hub.set_primary_temperature_sensor)
|
||||
await sensors(CONF_SECONDARY_TEMPERATURE, hub.set_secondary_temperature_sensor)
|
||||
await sensors(CONF_TARGET_TEMPERATURE, hub.set_target_temperature_sensor)
|
||||
await sensors(CONF_ESTIMATED_HEATING_TIME, hub.set_estimated_heating_time_sensor)
|
||||
await sensors(CONF_AVAILABLE_WATER, hub.set_available_water_sensor)
|
||||
@@ -0,0 +1,20 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MODE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .water_heater import CONF_HAIER_WATER_HEATER_ID, HonWaterHeater
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HAIER_WATER_HEATER_ID): cv.use_id(HonWaterHeater),
|
||||
cv.Optional(CONF_MODE): text_sensor.text_sensor_schema(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_HAIER_WATER_HEATER_ID])
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
await text_sensors(CONF_MODE, hub.set_mode_text_sensor)
|
||||
@@ -1,48 +1,21 @@
|
||||
"""Local controller for Haier hOn water heaters."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, sensor, text_sensor, uart, water_heater
|
||||
from esphome.components import uart, water_heater
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_MODE,
|
||||
CONF_POWER,
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_HEAT,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_MINUTE,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_AVAILABLE_WATER = "available_water"
|
||||
CONF_COMMUNICATION = "communication"
|
||||
CONF_COMMUNICATION_TIMEOUT = "communication_timeout"
|
||||
CONF_ESTIMATED_HEATING_TIME = "estimated_heating_time"
|
||||
CONF_HEATING = "heating"
|
||||
CONF_PRIMARY_TEMPERATURE = "primary_temperature"
|
||||
CONF_SECONDARY_TEMPERATURE = "secondary_temperature"
|
||||
CONF_HAIER_WATER_HEATER_ID = "haier_water_heater_id"
|
||||
|
||||
CODEOWNERS = ["@TheLexus"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor"]
|
||||
|
||||
haier_water_heater_ns = cg.esphome_ns.namespace("haier_water_heater")
|
||||
HonWaterHeater = haier_water_heater_ns.class_(
|
||||
"HonWaterHeater", water_heater.WaterHeater, cg.Component, uart.UARTDevice
|
||||
)
|
||||
|
||||
_TEMPERATURE_SENSOR = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
water_heater.water_heater_schema(HonWaterHeater)
|
||||
.extend(
|
||||
@@ -50,29 +23,6 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(
|
||||
CONF_COMMUNICATION_TIMEOUT, default="20s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_PRIMARY_TEMPERATURE): _TEMPERATURE_SENSOR,
|
||||
cv.Optional(CONF_SECONDARY_TEMPERATURE): _TEMPERATURE_SENSOR,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): _TEMPERATURE_SENSOR,
|
||||
cv.Optional(CONF_ESTIMATED_HEATING_TIME): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MINUTE,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_AVAILABLE_WATER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_COMMUNICATION): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_POWER): binary_sensor.binary_sensor_schema(),
|
||||
cv.Optional(CONF_HEATING): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_HEAT
|
||||
),
|
||||
cv.Optional(CONF_MODE): text_sensor.text_sensor_schema(),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -95,16 +45,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_communication_timeout(config[CONF_COMMUNICATION_TIMEOUT]))
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_PRIMARY_TEMPERATURE, var.set_primary_temperature_sensor)
|
||||
await sensors(CONF_SECONDARY_TEMPERATURE, var.set_secondary_temperature_sensor)
|
||||
await sensors(CONF_TARGET_TEMPERATURE, var.set_target_temperature_sensor)
|
||||
await sensors(CONF_ESTIMATED_HEATING_TIME, var.set_estimated_heating_time_sensor)
|
||||
await sensors(CONF_AVAILABLE_WATER, var.set_available_water_sensor)
|
||||
binary_sensors = binary_sensor.sub_binary_sensors(config)
|
||||
await binary_sensors(CONF_COMMUNICATION, var.set_communication_sensor)
|
||||
await binary_sensors(CONF_POWER, var.set_power_sensor)
|
||||
await binary_sensors(CONF_HEATING, var.set_heating_sensor)
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
await text_sensors(CONF_MODE, var.set_mode_sensor)
|
||||
@@ -33,7 +33,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
|
||||
DECAY_MODE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): fan.validate_speed_count,
|
||||
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ ESPHOME_LOG_TAG(TAG, "switch.hbridge");
|
||||
|
||||
float HBridgeSwitch::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
void HBridgeSwitch::setup() {
|
||||
optional<bool> initial_state = this->get_initial_state_with_restore_mode();
|
||||
|
||||
// Like GPIOSwitch does, set the pin state both before and after pin setup()
|
||||
this->on_pin_->digital_write(false);
|
||||
this->on_pin_->setup();
|
||||
@@ -20,8 +18,7 @@ void HBridgeSwitch::setup() {
|
||||
this->off_pin_->setup();
|
||||
this->off_pin_->digital_write(false);
|
||||
|
||||
if (initial_state.has_value())
|
||||
this->write_state(initial_state.value());
|
||||
this->restore();
|
||||
}
|
||||
|
||||
void HBridgeSwitch::dump_config() {
|
||||
|
||||
@@ -155,12 +155,9 @@ void HitachiClimate::setup() {
|
||||
set_swing_v_(swing_vertical(restore->swing_mode));
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
// The restored value is the switch state, which an inverted switch flips relative to the frame bit
|
||||
if (this->mildew_proof_switch_ != nullptr) {
|
||||
if (auto initial = this->mildew_proof_switch_->get_initial_state_with_restore_mode()) {
|
||||
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET,
|
||||
*initial != this->mildew_proof_switch_->is_inverted());
|
||||
}
|
||||
if (auto initial = this->mildew_proof_switch_->get_initial_write_state())
|
||||
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET, *initial);
|
||||
}
|
||||
#endif
|
||||
this->setup_(restore);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "hoermann_hcp_text_sensor.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
@@ -37,13 +35,7 @@ void HoermannHcpDoorStateTextSensor::update_from_state_() {
|
||||
return;
|
||||
this->published_ = true;
|
||||
this->published_state_ = state;
|
||||
char text[16];
|
||||
ESPHOME_strncpy_P(text,
|
||||
reinterpret_cast<ESPHOME_PGM_P>(
|
||||
DoorStateStrings::get_progmem_str(static_cast<uint8_t>(state), DoorStateStrings::LAST_INDEX)),
|
||||
sizeof(text));
|
||||
text[sizeof(text) - 1] = '\0';
|
||||
this->publish_state(text);
|
||||
this->publish_state(DoorStateStrings::get_progmem_str(static_cast<uint8_t>(state), DoorStateStrings::LAST_INDEX));
|
||||
}
|
||||
|
||||
} // namespace esphome::hoermann_hcp
|
||||
@@ -15,25 +15,34 @@ ESPHOME_LOG_TAG(TAG, "http_request.ota");
|
||||
|
||||
void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
|
||||
|
||||
OtaHttpRequestComponent::Request &OtaHttpRequestComponent::get_request_() {
|
||||
if (this->request_ == nullptr)
|
||||
this->request_ = make_unique<Request>();
|
||||
return *this->request_;
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
|
||||
if (!this->validate_url_(url)) {
|
||||
this->md5_url_.clear(); // URL was not valid; prevent flashing until it is
|
||||
if (this->request_ != nullptr)
|
||||
this->request_->md5_url.clear(); // URL was not valid; prevent flashing until it is
|
||||
return;
|
||||
}
|
||||
this->md5_url_ = url;
|
||||
this->md5_expected_.clear(); // to be retrieved later
|
||||
Request &request = this->get_request_();
|
||||
request.md5_url = url;
|
||||
request.md5_expected.clear(); // to be retrieved later
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::set_url(const std::string &url) {
|
||||
if (!this->validate_url_(url)) {
|
||||
this->url_.clear(); // URL was not valid; prevent flashing until it is
|
||||
if (this->request_ != nullptr)
|
||||
this->request_->url.clear(); // URL was not valid; prevent flashing until it is
|
||||
return;
|
||||
}
|
||||
this->url_ = url;
|
||||
this->get_request_().url = url;
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::flash() {
|
||||
if (this->url_.empty()) {
|
||||
if (this->request_ == nullptr || this->request_->url.empty()) {
|
||||
ESP_LOGE(TAG, "URL not set; cannot start update");
|
||||
return;
|
||||
}
|
||||
@@ -58,8 +67,8 @@ void OtaHttpRequestComponent::flash() {
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status);
|
||||
#endif
|
||||
this->md5_computed_.clear(); // will be reset at next attempt
|
||||
this->md5_expected_.clear(); // will be reset at next attempt
|
||||
this->request_->md5_computed.clear(); // will be reset at next attempt
|
||||
this->request_->md5_expected.clear(); // will be reset at next attempt
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -81,18 +90,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
md5::MD5Digest md5_receive;
|
||||
char md5_receive_str[33];
|
||||
|
||||
if (this->md5_expected_.empty() && !this->http_get_md5_()) {
|
||||
if (this->request_->md5_expected.empty() && !this->http_get_md5_()) {
|
||||
return OTA_MD5_INVALID;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "MD5 expected: %s", this->md5_expected_.c_str());
|
||||
ESP_LOGD(TAG, "MD5 expected: %s", this->request_->md5_expected.c_str());
|
||||
|
||||
auto url_with_auth = this->get_url_with_auth_(this->url_);
|
||||
auto url_with_auth = this->get_url_with_auth_(this->request_->url);
|
||||
if (url_with_auth.empty()) {
|
||||
return OTA_BAD_URL;
|
||||
}
|
||||
ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
|
||||
ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str());
|
||||
ESP_LOGI(TAG, "Connecting to: %s", this->request_->url.c_str());
|
||||
|
||||
auto container = this->parent_->get(url_with_auth);
|
||||
|
||||
@@ -179,9 +188,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
// verify MD5 is as expected and act accordingly
|
||||
md5_receive.calculate();
|
||||
md5_receive.get_hex(md5_receive_str);
|
||||
this->md5_computed_ = md5_receive_str;
|
||||
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
|
||||
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
|
||||
this->request_->md5_computed = md5_receive_str;
|
||||
if (strncmp(this->request_->md5_computed.c_str(), this->request_->md5_expected.c_str(), MD5_SIZE) != 0) {
|
||||
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->request_->md5_computed.c_str());
|
||||
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
|
||||
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
|
||||
} else {
|
||||
@@ -223,11 +232,15 @@ static std::string url_encode(const std::string &str) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); }
|
||||
void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); }
|
||||
void OtaHttpRequestComponent::set_password(const std::string &password) {
|
||||
this->get_request_().password = url_encode(password);
|
||||
}
|
||||
void OtaHttpRequestComponent::set_username(const std::string &username) {
|
||||
this->get_request_().username = url_encode(username);
|
||||
}
|
||||
|
||||
std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) {
|
||||
if (this->username_.empty() || this->password_.empty()) {
|
||||
if (this->request_->username.empty() || this->request_->password.empty()) {
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -240,23 +253,23 @@ std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url)
|
||||
ESP_LOGD(TAG, "Using basic HTTP authentication");
|
||||
|
||||
start_char += 3; // skip '://' characters
|
||||
auto url_with_auth =
|
||||
url.substr(0, start_char) + this->username_ + ":" + this->password_ + "@" + url.substr(start_char);
|
||||
auto url_with_auth = url.substr(0, start_char) + this->request_->username + ":" + this->request_->password + "@" +
|
||||
url.substr(start_char);
|
||||
return url_with_auth;
|
||||
}
|
||||
|
||||
bool OtaHttpRequestComponent::http_get_md5_() {
|
||||
if (this->md5_url_.empty()) {
|
||||
if (this->request_->md5_url.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto url_with_auth = this->get_url_with_auth_(this->md5_url_);
|
||||
auto url_with_auth = this->get_url_with_auth_(this->request_->md5_url);
|
||||
if (url_with_auth.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
|
||||
ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str());
|
||||
ESP_LOGI(TAG, "Connecting to: %s", this->request_->md5_url.c_str());
|
||||
auto container = this->parent_->get(url_with_auth);
|
||||
if (container == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to connect to MD5 URL");
|
||||
@@ -273,8 +286,8 @@ bool OtaHttpRequestComponent::http_get_md5_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->md5_expected_.resize(MD5_SIZE);
|
||||
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
|
||||
this->request_->md5_expected.resize(MD5_SIZE);
|
||||
auto result = http_read_fully(container.get(), (uint8_t *) this->request_->md5_expected.data(), MD5_SIZE, MD5_SIZE,
|
||||
this->parent_->get_timeout());
|
||||
container->end();
|
||||
|
||||
|
||||
@@ -27,29 +27,36 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void set_md5_url(const std::string &md5_url);
|
||||
void set_md5(const std::string &md5) { this->md5_expected_ = md5; }
|
||||
void set_md5(const std::string &md5) { this->get_request_().md5_expected = md5; }
|
||||
void set_password(const std::string &password);
|
||||
void set_url(const std::string &url);
|
||||
void set_username(const std::string &username);
|
||||
|
||||
std::string md5_computed() { return this->md5_computed_; }
|
||||
std::string md5_expected() { return this->md5_expected_; }
|
||||
std::string md5_computed() { return this->request_ != nullptr ? this->request_->md5_computed : std::string(); }
|
||||
std::string md5_expected() { return this->request_ != nullptr ? this->request_->md5_expected : std::string(); }
|
||||
|
||||
void flash();
|
||||
|
||||
protected:
|
||||
/// Allocated by the first setter, which only runs before a flash, and never freed, so a flash never loses it.
|
||||
struct Request {
|
||||
std::string md5_computed;
|
||||
std::string md5_expected;
|
||||
std::string md5_url;
|
||||
std::string password;
|
||||
std::string username;
|
||||
std::string url;
|
||||
};
|
||||
Request &get_request_();
|
||||
|
||||
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container, bool abort_backend);
|
||||
// Only reached from flash(), which returns early while request_ is null
|
||||
uint8_t do_ota_();
|
||||
std::string get_url_with_auth_(const std::string &url);
|
||||
bool http_get_md5_();
|
||||
bool validate_url_(const std::string &url);
|
||||
|
||||
std::string md5_computed_{};
|
||||
std::string md5_expected_{};
|
||||
std::string md5_url_{};
|
||||
std::string password_{};
|
||||
std::string username_{};
|
||||
std::string url_{};
|
||||
std::unique_ptr<Request> request_;
|
||||
int status_ = -1;
|
||||
static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size
|
||||
};
|
||||
|
||||
@@ -163,21 +163,19 @@ static constexpr uint8_t CMD_DURATION_VALUE = 0x02;
|
||||
// Bitmasks for target states
|
||||
static constexpr uint8_t MOVE_BITMASK = 0x01;
|
||||
static constexpr uint8_t STILL_BITMASK = 0x02;
|
||||
// Header & Footer size
|
||||
static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
|
||||
// Command Header & Footer
|
||||
static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
|
||||
static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
|
||||
using ld24xx::CMD_FRAME_FOOTER;
|
||||
using ld24xx::CMD_FRAME_HEADER;
|
||||
using ld24xx::HEADER_FOOTER_SIZE;
|
||||
using ld24xx::validate_header_footer;
|
||||
// Smallest frames whose values can all be read: the last value, data footer and check, then the frame footer
|
||||
static constexpr uint8_t PERIODIC_FRAME_MIN = DETECT_DISTANCE_HIGH + 3 + HEADER_FOOTER_SIZE;
|
||||
static constexpr uint8_t ENGINEERING_FRAME_MIN = OUT_PIN_SENSOR + 3 + HEADER_FOOTER_SIZE;
|
||||
// Data Header & Footer
|
||||
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1};
|
||||
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 inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
|
||||
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
|
||||
}
|
||||
|
||||
void LD2410Component::dump_config() {
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
char version_s[20];
|
||||
@@ -328,6 +326,9 @@ void LD2410Component::handle_periodic_data_() {
|
||||
0x02: Normal mode
|
||||
*/
|
||||
bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01;
|
||||
if (this->buffer_pos_ < (engineering_mode ? ENGINEERING_FRAME_MIN : PERIODIC_FRAME_MIN)) {
|
||||
return; // too short to hold the values
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
if (this->engineering_mode_switch_ != nullptr) {
|
||||
this->engineering_mode_switch_->publish_state(engineering_mode);
|
||||
@@ -414,24 +415,15 @@ void set_number_value(number::Number *n, float value) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool LD2410Component::handle_ack_data_() {
|
||||
void LD2410Component::handle_ack_data_() {
|
||||
ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
|
||||
if (this->buffer_pos_ < 10) {
|
||||
ESP_LOGE(TAG, "Invalid length");
|
||||
return true;
|
||||
}
|
||||
if (!ld2410::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
|
||||
char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)];
|
||||
ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE));
|
||||
return true;
|
||||
}
|
||||
if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
|
||||
ESP_LOGE(TAG, "Invalid status");
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
if (this->buffer_data_[8] || this->buffer_data_[9]) {
|
||||
ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this->buffer_data_[COMMAND]) {
|
||||
@@ -454,6 +446,9 @@ bool LD2410Component::handle_ack_data_() {
|
||||
break;
|
||||
|
||||
case CMD_QUERY_VERSION: {
|
||||
if (this->buffer_pos_ < 12 + sizeof(this->version_) + HEADER_FOOTER_SIZE) {
|
||||
break; // too short to hold a version
|
||||
}
|
||||
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
|
||||
char version_s[20];
|
||||
ld24xx::format_version_str(this->version_, version_s);
|
||||
@@ -467,6 +462,9 @@ bool LD2410Component::handle_ack_data_() {
|
||||
}
|
||||
|
||||
case CMD_QUERY_DISTANCE_RESOLUTION: {
|
||||
if (this->buffer_pos_ < 11 + HEADER_FOOTER_SIZE) {
|
||||
break; // too short to hold the resolution
|
||||
}
|
||||
const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
|
||||
ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
|
||||
#ifdef USE_SELECT
|
||||
@@ -478,6 +476,9 @@ bool LD2410Component::handle_ack_data_() {
|
||||
}
|
||||
|
||||
case CMD_QUERY_LIGHT_CONTROL: {
|
||||
if (this->buffer_pos_ < 13 + HEADER_FOOTER_SIZE) {
|
||||
break; // too short to hold the light settings
|
||||
}
|
||||
this->light_function_ = this->buffer_data_[10];
|
||||
this->light_threshold_ = this->buffer_data_[11];
|
||||
this->out_pin_level_ = this->buffer_data_[12];
|
||||
@@ -502,7 +503,7 @@ bool LD2410Component::handle_ack_data_() {
|
||||
}
|
||||
case CMD_QUERY_MAC_ADDRESS: {
|
||||
if (this->buffer_pos_ < 20) {
|
||||
return false;
|
||||
break; // too short to hold a MAC address
|
||||
}
|
||||
|
||||
this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
|
||||
@@ -547,12 +548,11 @@ bool LD2410Component::handle_ack_data_() {
|
||||
break;
|
||||
|
||||
case CMD_QUERY: { // Query parameters response
|
||||
// Config values can contain the footer bytes, so wait for the whole 38 byte frame
|
||||
if (this->buffer_pos_ < 38) {
|
||||
return false;
|
||||
break; // too short to hold the parameters
|
||||
}
|
||||
if (this->buffer_data_[10] != HEADER)
|
||||
return true; // value head=0xAA
|
||||
return; // value head=0xAA
|
||||
#ifdef USE_NUMBER
|
||||
/*
|
||||
Moving distance range: 13th byte
|
||||
@@ -582,39 +582,36 @@ bool LD2410Component::handle_ack_data_() {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void LD2410Component::readline_(int readch) {
|
||||
if (readch < 0) {
|
||||
return; // No data available
|
||||
}
|
||||
// Periodic and command frames both carry their count after the header
|
||||
ld24xx::feed_frame_byte(
|
||||
this->buffer_data_, this->buffer_pos_, readch, DATA_FRAME_HEADER, ld24xx::cmd_frame_length,
|
||||
[this](uint8_t frame_len) { return this->handle_frame_(frame_len); }, []() { ESP_LOGW(TAG, "Invalid length"); });
|
||||
}
|
||||
|
||||
if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
|
||||
this->buffer_data_[this->buffer_pos_++] = readch;
|
||||
this->buffer_data_[this->buffer_pos_] = 0;
|
||||
} else {
|
||||
// We should never get here, but just in case...
|
||||
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
|
||||
this->buffer_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
if (this->buffer_pos_ < HEADER_FOOTER_SIZE) {
|
||||
return; // Not enough data to process yet
|
||||
}
|
||||
if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
|
||||
// Dispatches the frame of frame_len bytes at the start of the buffer when its footer is in place
|
||||
bool LD2410Component::handle_frame_(uint8_t frame_len) {
|
||||
this->buffer_pos_ = frame_len; // the handlers take the frame length from here
|
||||
const uint8_t *tail = &this->buffer_data_[frame_len - HEADER_FOOTER_SIZE];
|
||||
if (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) {
|
||||
if (!ld2410::validate_header_footer(DATA_FRAME_FOOTER, tail)) {
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Periodic Data: %s");
|
||||
this->handle_periodic_data_();
|
||||
this->buffer_pos_ = 0; // Reset position index for next message
|
||||
} else if (ld2410::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
|
||||
ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Ack Data: %s");
|
||||
if (this->handle_ack_data_()) {
|
||||
this->buffer_pos_ = 0; // Reset position index for next message
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Ack Data incomplete");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!ld2410::validate_header_footer(CMD_FRAME_FOOTER, tail)) {
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Ack Data: %s");
|
||||
this->handle_ack_data_();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LD2410Component::set_config_mode_(bool enable) {
|
||||
|
||||
@@ -106,7 +106,8 @@ class LD2410Component final : public Component, public uart::UARTDevice {
|
||||
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
|
||||
void set_config_mode_(bool enable);
|
||||
void handle_periodic_data_();
|
||||
bool handle_ack_data_();
|
||||
void handle_ack_data_();
|
||||
bool handle_frame_(uint8_t frame_len);
|
||||
void readline_(int readch);
|
||||
void query_parameters_();
|
||||
void get_version_();
|
||||
|
||||
@@ -186,23 +186,19 @@ static constexpr uint8_t CMD_DURATION_VALUE = 0x02;
|
||||
// Bitmasks for target states
|
||||
static constexpr uint8_t MOVE_BITMASK = 0x01;
|
||||
static constexpr uint8_t STILL_BITMASK = 0x02;
|
||||
// Header & Footer size
|
||||
static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
|
||||
// Command Header & Footer
|
||||
static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
|
||||
static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
|
||||
using ld24xx::CMD_FRAME_FOOTER;
|
||||
using ld24xx::CMD_FRAME_HEADER;
|
||||
using ld24xx::HEADER_FOOTER_SIZE;
|
||||
using ld24xx::validate_header_footer;
|
||||
// Smallest frames whose values can all be read: the last value, data footer and check, then the frame footer
|
||||
static constexpr uint8_t PERIODIC_FRAME_MIN = STILL_ENERGY + 3 + HEADER_FOOTER_SIZE;
|
||||
static constexpr uint8_t ENGINEERING_FRAME_MIN = LIGHT_SENSOR + 3 + HEADER_FOOTER_SIZE;
|
||||
// Data Header & Footer
|
||||
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1};
|
||||
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;
|
||||
}
|
||||
|
||||
void LD2412Component::dump_config() {
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
@@ -371,6 +367,9 @@ void LD2412Component::handle_periodic_data_() {
|
||||
0x02: Normal mode
|
||||
*/
|
||||
bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01;
|
||||
if (this->buffer_pos_ < PERIODIC_FRAME_MIN) {
|
||||
return; // too short to hold the values
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
if (this->engineering_mode_switch_ != nullptr) {
|
||||
this->engineering_mode_switch_->publish_state(engineering_mode);
|
||||
@@ -421,8 +420,7 @@ void LD2412Component::handle_periodic_data_() {
|
||||
this->detection_distance_sensor_.publish_state_if_not_dup(new_detect_distance);
|
||||
}
|
||||
if (engineering_mode) {
|
||||
// Engineering mode needs at least LIGHT_SENSOR + 1 bytes
|
||||
if (this->buffer_pos_ < LIGHT_SENSOR + 1) {
|
||||
if (this->buffer_pos_ < ENGINEERING_FRAME_MIN) {
|
||||
ESP_LOGW(TAG, "Engineering mode packet too short: %u", this->buffer_pos_);
|
||||
} else {
|
||||
/*
|
||||
@@ -477,24 +475,15 @@ static constexpr uint32_t GATE_QUERY_TIMEOUT_ID = 0;
|
||||
static constexpr uint32_t GATE_QUERY_TIMEOUT_MS = 2000;
|
||||
#endif
|
||||
|
||||
bool LD2412Component::handle_ack_data_() {
|
||||
void LD2412Component::handle_ack_data_() {
|
||||
ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
|
||||
if (this->buffer_pos_ < 10) {
|
||||
ESP_LOGW(TAG, "Invalid length");
|
||||
return true;
|
||||
}
|
||||
if (!ld2412::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
|
||||
char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)];
|
||||
ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE));
|
||||
return true;
|
||||
}
|
||||
if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
|
||||
ESP_LOGW(TAG, "Invalid status");
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
if (this->buffer_data_[8] || this->buffer_data_[9]) {
|
||||
ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this->buffer_data_[COMMAND]) {
|
||||
@@ -517,8 +506,8 @@ bool LD2412Component::handle_ack_data_() {
|
||||
break;
|
||||
|
||||
case CMD_QUERY_VERSION: {
|
||||
if (this->buffer_pos_ < 12 + sizeof(this->version_)) {
|
||||
return false;
|
||||
if (this->buffer_pos_ < 12 + sizeof(this->version_) + HEADER_FOOTER_SIZE) {
|
||||
break; // too short to hold a version
|
||||
}
|
||||
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
|
||||
char version_s[20];
|
||||
@@ -532,6 +521,9 @@ bool LD2412Component::handle_ack_data_() {
|
||||
break;
|
||||
}
|
||||
case CMD_QUERY_DISTANCE_RESOLUTION: {
|
||||
if (this->buffer_pos_ < 11 + HEADER_FOOTER_SIZE) {
|
||||
break; // too short to hold the resolution
|
||||
}
|
||||
const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
|
||||
ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
|
||||
#ifdef USE_SELECT
|
||||
@@ -543,6 +535,9 @@ bool LD2412Component::handle_ack_data_() {
|
||||
}
|
||||
|
||||
case CMD_QUERY_LIGHT_CONTROL: {
|
||||
if (this->buffer_pos_ < 12 + HEADER_FOOTER_SIZE) {
|
||||
break; // too short to hold the light settings
|
||||
}
|
||||
this->light_function_ = this->buffer_data_[10];
|
||||
this->light_threshold_ = this->buffer_data_[11];
|
||||
const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
|
||||
@@ -562,7 +557,7 @@ bool LD2412Component::handle_ack_data_() {
|
||||
|
||||
case CMD_QUERY_MAC_ADDRESS: {
|
||||
if (this->buffer_pos_ < 20) {
|
||||
return false;
|
||||
break; // too short to hold a MAC address
|
||||
}
|
||||
|
||||
this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
|
||||
@@ -591,6 +586,9 @@ bool LD2412Component::handle_ack_data_() {
|
||||
break;
|
||||
|
||||
case CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION: {
|
||||
if (this->buffer_pos_ < 11 + HEADER_FOOTER_SIZE) {
|
||||
break; // too short to hold the state
|
||||
}
|
||||
ESP_LOGV(TAG, "Handled query dynamic background correction");
|
||||
bool dynamic_background_correction_active = (this->buffer_data_[10] != 0x00);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -613,14 +611,10 @@ bool LD2412Component::handle_ack_data_() {
|
||||
case CMD_QUERY_MOTION_GATE_SENS:
|
||||
case CMD_QUERY_STATIC_GATE_SENS: {
|
||||
#ifdef USE_NUMBER
|
||||
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
|
||||
if (this->buffer_pos_ != GATE_SENS_ACK_SIZE) {
|
||||
// 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
|
||||
return;
|
||||
}
|
||||
const bool motion = this->buffer_data_[COMMAND] == CMD_QUERY_MOTION_GATE_SENS;
|
||||
GateThresholds &group = this->gate_thresholds_(motion);
|
||||
@@ -646,8 +640,8 @@ bool LD2412Component::handle_ack_data_() {
|
||||
case CMD_QUERY_BASIC_CONF: // Query parameters response
|
||||
{
|
||||
#ifdef USE_NUMBER
|
||||
if (this->buffer_pos_ < 15) {
|
||||
return false;
|
||||
if (this->buffer_pos_ < 15 + HEADER_FOOTER_SIZE) {
|
||||
break; // too short to hold the configuration
|
||||
}
|
||||
/*
|
||||
Moving distance range: 9th byte
|
||||
@@ -677,42 +671,36 @@ bool LD2412Component::handle_ack_data_() {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void LD2412Component::readline_(int readch) {
|
||||
if (readch < 0) {
|
||||
return; // No data available
|
||||
}
|
||||
if (this->buffer_pos_ < HEADER_FOOTER_SIZE && readch != DATA_FRAME_HEADER[this->buffer_pos_] &&
|
||||
readch != CMD_FRAME_HEADER[this->buffer_pos_]) {
|
||||
this->buffer_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
|
||||
this->buffer_data_[this->buffer_pos_++] = readch;
|
||||
this->buffer_data_[this->buffer_pos_] = 0;
|
||||
} else {
|
||||
// We should never get here, but just in case...
|
||||
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
if (this->buffer_pos_ < 4) {
|
||||
return; // Not enough data to process yet
|
||||
}
|
||||
if (ld2412::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
|
||||
// Periodic and command frames both carry their count after the header
|
||||
ld24xx::feed_frame_byte(
|
||||
this->buffer_data_, this->buffer_pos_, readch, DATA_FRAME_HEADER, ld24xx::cmd_frame_length,
|
||||
[this](uint8_t frame_len) { return this->handle_frame_(frame_len); }, []() { ESP_LOGW(TAG, "Invalid length"); });
|
||||
}
|
||||
|
||||
// Dispatches the frame of frame_len bytes at the start of the buffer when its footer is in place
|
||||
bool LD2412Component::handle_frame_(uint8_t frame_len) {
|
||||
this->buffer_pos_ = frame_len; // the handlers take the frame length from here
|
||||
const uint8_t *tail = &this->buffer_data_[frame_len - HEADER_FOOTER_SIZE];
|
||||
if (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) {
|
||||
if (!ld2412::validate_header_footer(DATA_FRAME_FOOTER, tail)) {
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Periodic Data: %s");
|
||||
this->handle_periodic_data_();
|
||||
this->buffer_pos_ = 0; // Reset position index for next message
|
||||
} else if (ld2412::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
|
||||
ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Ack Data: %s");
|
||||
if (this->handle_ack_data_()) {
|
||||
this->buffer_pos_ = 0; // Reset position index for next message
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Ack Data incomplete");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!ld2412::validate_header_footer(CMD_FRAME_FOOTER, tail)) {
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Ack Data: %s");
|
||||
this->handle_ack_data_();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LD2412Component::set_config_mode_(bool enable) {
|
||||
|
||||
@@ -111,7 +111,8 @@ class LD2412Component final : public Component, public uart::UARTDevice {
|
||||
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
|
||||
void set_config_mode_(bool enable);
|
||||
void handle_periodic_data_();
|
||||
bool handle_ack_data_();
|
||||
void handle_ack_data_();
|
||||
bool handle_frame_(uint8_t frame_len);
|
||||
void readline_(int readch);
|
||||
void query_parameters_();
|
||||
void get_version_();
|
||||
|
||||
@@ -15,6 +15,15 @@ LD2420Component = ld2420_ns.class_("LD2420Component", cg.Component, uart.UARTDev
|
||||
|
||||
CONF_LD2420_ID = "ld2420_id"
|
||||
|
||||
# Each hub keeps its listeners in a fixed size list; the define is the largest count any one hub needs
|
||||
_request_listener_slot = cg.slot_counter("LD2420_LISTENER_COUNT")
|
||||
|
||||
|
||||
async def register_listener(hub: cg.MockObj, var: cg.MockObj) -> None:
|
||||
_request_listener_slot(str(hub))
|
||||
cg.add(hub.register_listener(var))
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_HAS_TARGET, CONF_ID, DEVICE_CLASS_OCCUPANCY
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns
|
||||
from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns, register_listener
|
||||
|
||||
LD2420BinarySensor = ld2420_ns.class_(
|
||||
"LD2420BinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||
@@ -30,4 +30,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
binary_sensors = binary_sensor.sub_binary_sensors(config)
|
||||
await binary_sensors(CONF_HAS_TARGET, var.set_presence_sensor)
|
||||
hub = await cg.get_variable(config[CONF_LD2420_ID])
|
||||
cg.add(hub.register_listener(var))
|
||||
await register_listener(hub, var)
|
||||
Loaded 100 of 354 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user