Compare commits

...
Author SHA1 Message Date
J. Nick Koston f228b21746 Merge remote-tracking branch 'origin/yaml-edit-apply' into cli-rotate-key 2026-10-26 04:40:28 -10:00
J. Nick Koston 970885b79b Merge remote-tracking branch 'origin/dev' into yaml-edit-apply
# Conflicts:
#	esphome/yaml_util.py
2026-10-26 04:40:20 -10:00
J. Nick Koston db6c956e55 [core] Add the YAML loader helpers store_yaml builds on (#21432) 2026-10-26 04:39:25 -10:00
J. Nick Koston c95ce587c0 [components] Use Apply outputs for hub channel outputs (#21257) 2026-10-26 04:38:23 -10:00
J. Nick Koston 3540ac4d1f [esp32] Describe each variant's GPIO rules with a pin spec (#21463) 2026-10-26 14:37:43 +00:00
J. Nick Koston 180d7b31d8 [components] Use shared helpers in recently added components (#21024) 2026-10-26 10:35:25 -04:00
J. Nick Koston 3cf79df27c [components] Use update_register and write_registers in the remaining I2C components (#21083) 2026-10-26 10:34:45 -04:00
J. Nick Kostonandpre-commit-ci-lite[bot] 2beb2de8a1 [web_server] Pass entity domain names from flash (#21115)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-26 10:33:04 -04:00
J. Nick Koston 9e457b11ba [dfrobot_sen0395] Use Apply switches (#21253) 2026-10-26 10:30:39 -04:00
J. Nick Koston ee41748744 [ld2420] Use fixed size storage and keep the mode strings in flash (#21347) 2026-10-26 10:30:11 -04:00
J. Nick Koston 08d4d59b1f [ld2450] Keep the baud rate, direction and zone type names in flash (#21349) 2026-10-26 10:29:29 -04:00
J. Nick Koston e4c0ef7c95 [ld24xx] Keep the unknown MAC text and version format in flash (#21352) 2026-10-26 10:22:42 -04:00
J. Nick Koston 26ee68652f [core] Add FrameReader to collect fixed length binary frames (#21446) 2026-10-26 04:20:59 -10:00
J. Nick Koston fa88c9218b [ld2412] Split frames by their length instead of scanning for footer bytes (#21360) 2026-10-26 04:19:37 -10:00
J. Nick Koston 44e27c08d8 [ld2450] Split frames by their length instead of scanning for footer bytes (#21357) 2026-10-26 04:19:20 -10:00
J. Nick Koston f431cdfb0c [haier_water_heater] Move child entities to sensor, binary_sensor and text_sensor platforms (#21437) 2026-10-26 10:18:05 -04:00
J. Nick Koston ba488eab6e [components] Apply inverted when restoring gree, hbridge, lvgl, opentherm and haier_tundra switches (#21544) 2026-10-26 04:16:56 -10:00
J. Nick Koston f21183c160 [esp32] Add a data driven pin spec for the variant GPIO validators (#21461) 2026-10-26 10:15:09 -04:00
J. Nick Koston 60ca75baad [ld2410] Split frames by their length instead of scanning for footer bytes (#21361) 2026-10-26 04:15:00 -10:00
J. Nick Koston b668cb1e50 [usb_uart] Use FixedRingBuffer for the receive buffer (#21476) 2026-10-26 10:14:32 -04:00
J. Nick Koston ab7d0ca17d [ltr390] Pack members, drop the float tables and expand gain and resolution in the schema (#21195) 2026-10-26 14:11:02 +00:00
J. Nick Koston 43f10a7cfe [core] Turn off the precompiled header with ccache older than 4.7 (#21627) 2026-10-26 04:09:55 -10:00
J. Nick Koston d5e755e407 [dfrobot_sen0395] Queue commands by value instead of allocating them on the heap (#20906) 2026-10-26 04:09:07 -10:00
J. Nick Koston cf67610d43 [xiaomi_ble] Move the Xiaomi sensors onto XiaomiDevice and fix the shared duplicate filter (#21483) 2026-10-26 04:08:39 -10:00
J. Nick Koston bd55bdcd6c [sen6x] Drop the product and serial strings (#21199) 2026-10-26 10:05:57 -04:00
J. Nick Koston f42d1ac645 [airton] Use an Apply select for the vane direction (#21252) 2026-10-26 10:05:13 -04:00
J. Nick Koston 5198c37cd9 [bedjet] Keep the device name in a fixed buffer (#21204) 2026-10-26 10:04:47 -04:00
J. Nick Koston d9fcc292e2 [components] Read hub-restored switch levels with get_initial_write_state (#21543) 2026-10-26 04:03:48 -10:00
J. Nick Koston 8f8e615ee7 [libretiny] Apply a level written before a pin becomes an output (#21603) 2026-10-26 04:03:25 -10:00
J. Nick Koston f91ad8d43c [ltr390] Wait the full integration time at 17, 16 and 13 bit (#21194) 2026-10-26 10:03:11 -04:00
J. Nick Koston 465a8b0386 [fan] Pack the fan traits flags and narrow the speed count to 16 bits (#21340) 2026-10-26 09:52:19 -04:00
J. Nick Koston 9e692b6772 [output] Add ApplyFloatOutput and ApplyBinaryOutput (#21255) 2026-10-26 09:49:49 -04:00
J. Nick Koston 1fd4025a0e [haier] Use Apply switches (#21251) 2026-10-26 09:46:30 -04:00
J. Nick Koston 03b8bd7941 [core] Add bulk push and pop to FixedRingBuffer (#21474) 2026-10-26 09:43:54 -04:00
J. Nick Koston 0e2c906fce [i2c] Put the device address after the bus pointer so derived classes reuse its padding (#21473) 2026-10-26 09:40:37 -04:00
J. Nick Koston d260265cf5 [ld6002b] Use StaticRingBuffer for the command queue (#21479) 2026-10-26 09:40:08 -04:00
J. Nick Koston 2388c3313d [xiaomi_ble] Add a shared XiaomiDevice base for MiBeacon devices (#21481) 2026-10-26 09:39:42 -04:00
J. Nick Koston e717a21bce [restart][shutdown][factory_reset][safe_mode] Use momentary Apply switches (#21519) 2026-10-26 09:37:12 -04:00
J. Nick Koston d47a510b84 [remote_base] Use the byte helpers in roomba, dooya and magiquest (#21513) 2026-10-26 09:09:36 -04:00
J. Nick Koston a16d0c48a0 [core] Add publish="momentary" for Apply switches that trigger an action (#21518) 2026-10-26 09:08:59 -04:00
J. Nick Koston 8541104d87 [switch] Add get_initial_write_state for hubs that restore their switches (#21542) 2026-10-26 09:04:41 -04:00
J. Nick Koston 579c5a5226 [components] Publish PROGMEM table strings on text sensors directly (#21537) 2026-10-26 09:02:38 -04:00
J. Nick Koston a027785fbf [libretiny] Bump LibreTiny to 1.14.0 and build BK7231N and BK7231T with BDK 3.0.78 (#21531) 2026-10-26 09:01:50 -04:00
J. Nick Koston d7f5976525 [http_request] Keep the OTA request strings out of RAM until a flash (#21556) 2026-10-26 09:00:04 -04:00
J. Nick Koston 4c8de264fd [remote_base] Keep the stateless dumpers in flash (#21563) 2026-10-26 08:59:08 -04:00
J. Nick Koston 73564dbebd A quoted include path keeps its hash in the shared-configuration scan 2026-10-17 10:37:14 -10:00
J. Nick Koston 4165dbb534 A comment needs whitespace before its hash in a secret name, old_key takes its secret from the ota block it lands on 2026-10-13 20:16:38 -10:00
J. Nick Koston 3bc9f74f46 The snapshot now carries the mode 2026-10-13 09:55:05 -10:00
J. Nick Koston 0dd27c5d08 Show the report an interrupted restore carries 2026-10-13 09:55:05 -10:00
J. Nick Koston 84b0f35b91 Keep the edit from the point the device accepted the session 2026-10-13 09:55:05 -10:00
J. Nick Koston 2aa3677bbf Advise by what the probe found: unreachable, another key, no encryption, a device error 2026-10-13 09:55:05 -10:00
J. Nick Koston c2fbcf0f24 Refuse a key that is not on a line of its own, name the sharers of an include that gets old_key 2026-10-13 09:55:05 -10:00
J. Nick Koston afbd2ab7b1 [cli] Add rotate-key to change the OTA encryption key over the air
Locates the current key on its yaml or secrets.yaml line, writes the new
one with the editor from #20602, keeps the replaced key as old_key, checks
the device with the probe from #20601, builds, uploads with the current key
and confirms with the new one; anything short of a reached device puts the
files back.
2026-10-13 09:55:04 -10:00
J. Nick Koston 3c65c55df0 Place a rebuilt mapping only when the key is on its value's line 2026-10-13 09:54:56 -10:00
J. Nick Koston 7704ad9384 Restore the mode from before the edit, finish the restore after any exception and raise an exit again with the report 2026-10-13 09:39:35 -10:00
J. Nick Koston 50fdc8e85b Reload a symlinked file by the path the loader read it by 2026-10-13 09:30:56 -10:00
J. Nick Koston 3e6324445b Match the restore report as text; a Windows path is no regex 2026-10-13 07:28:38 -10:00
J. Nick Koston e0e5cbdc27 Register a value written to a secrets file, keep the registry through own_documents, finish the restore after any failure and raise an interrupt again with the report 2026-10-13 07:28:38 -10:00
J. Nick Koston 870b80e463 Keep the secret registry on the reload, keep the order of insertions under one line, refuse an incomplete document set 2026-10-13 07:28:38 -10:00
J. Nick Koston 899e88be72 [core] Let the yaml editor insert lines, write with a rollback and restore
apply_line_edits rewrites the located lines in place, reloads every touched
file and drops the validated caches, undoing everything on any failure;
restore_files puts the originals back only over what was written. A key in
a mapping merge_config rebuilt is placed by its value's line.
2026-10-13 07:28:38 -10:00
354 changed files with 9227 additions and 5471 deletions

No files matched your search

+8 -3
View File
@@ -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`)
+38
View File
@@ -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
View File
@@ -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,
+48 -1
View File
@@ -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
)
+19 -4
View File
@@ -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:
+285
View File
@@ -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.",
)
)
+27 -1
View File
@@ -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:
+4
View File
@@ -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);
+4 -4
View File
@@ -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
+62 -73
View File
@@ -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 {};
+49 -47
View File
@@ -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);
+3 -10
View File
@@ -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.
+5 -9
View File
@@ -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
+54 -26
View File
@@ -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++
+2 -1
View File
@@ -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;
+1 -18
View File
@@ -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);
+5 -9
View File
@@ -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
+7 -12
View File
@@ -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() {
+2 -4
View File
@@ -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};
+6 -13
View File
@@ -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
+2 -23
View File
@@ -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] = {
+8 -11
View File
@@ -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]))
+18 -16
View File
@@ -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
+18 -14
View File
@@ -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() {
+3 -13
View File
@@ -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
+42 -91
View File
@@ -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)
+121 -18
View File
@@ -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
+24 -75
View File
@@ -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}),
)
+2 -32
View File
@@ -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}))
+17 -51
View File
@@ -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",
},
),
)
+19 -50
View File
@@ -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):
+19 -49
View File
@@ -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):
+19 -53
View File
@@ -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",
},
),
)
+12 -45
View File
@@ -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)
},
)
+9 -40
View File
@@ -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}),
)
+9 -40
View File
@@ -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}),
)
+7 -34
View File
@@ -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):
+24 -75
View File
@@ -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}),
)
+10 -61
View File
@@ -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:
+18 -51
View File
@@ -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):
+2 -2
View File
@@ -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,
@@ -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();
@@ -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
+4
View File
@@ -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)
+15 -11
View File
@@ -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()");
+1 -3
View File
@@ -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_() {
+8 -16
View File
@@ -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); }
+34 -23
View File
@@ -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
-16
View File
@@ -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
-16
View File
@@ -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)
+1 -1
View File
@@ -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
};
+45 -48
View File
@@ -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) {
+2 -1
View File
@@ -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_();
+52 -64
View File
@@ -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) {
+2 -1
View File
@@ -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_();
+9
View File
@@ -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