Compare commits

..
Author SHA1 Message Date
J. Nick Koston ebdbf61808 Merge remote-tracking branch 'origin/dev' into store-yaml-firmware 2026-10-26 05:51:45 -10:00
pre-commit-ci-lite[bot] 9e865fe8f7 apply automatic formatting fixes 2026-10-26 15:06:02 +00:00
J. Nick Koston 877124e29a [store_yaml] Declare the YAML blob in the api header and drop store_yaml.h 2026-10-26 05:03:18 -10:00
J. Nick Koston 953708ae1b Merge branch 'dev' into store-yaml-firmware 2026-10-26 04:56:40 -10:00
J. Nick Koston 489b2fe7f6 [store_yaml] Redact the modem pin_code 2026-10-26 04:54:32 -10:00
J. Nick Koston 7f0c4cfde1 Merge remote-tracking branch 'origin/store-yaml-core' into store-yaml-firmware
# Conflicts:
#	tests/integration/raw_api_client.py
2026-10-26 03:59:56 -10:00
J. Nick Koston c9807f3997 Merge remote-tracking branch 'origin/dev' into store-yaml-core 2026-10-26 03:59:07 -10:00
J. Nick Koston 34cc68e944 [store_yaml] Put each regex under its own comment 2026-10-22 15:51:30 -10:00
J. Nick Koston fe57ced916 [store_yaml] Match an inline lowercase bind key to its uppercased value 2026-10-22 15:34:14 -10:00
J. Nick Koston 6c81b51baf [store_yaml] Cover bind keys: name them as credentials and match a lowercase secret 2026-10-22 15:10:50 -10:00
J. Nick Koston 2cdc3f4b3d [store_yaml] Don't take include file names or one-segment directory fragments for credentials 2026-10-22 14:18:11 -10:00
J. Nick Koston 24605205f8 [store_yaml] Use the released GetYaml messages in the integration tests 2026-10-22 14:13:37 -10:00
J. Nick Koston 9ef5271113 Merge remote-tracking branch 'origin/store-yaml-core' into store-yaml-firmware 2026-10-22 14:12:33 -10:00
J. Nick Koston 80bf95f162 Merge remote-tracking branch 'origin/dev' into store-yaml-core 2026-10-22 14:12:04 -10:00
J. Nick Koston e44a16bc4d Merge remote-tracking branch 'origin/store-yaml-core' into store-yaml-firmware 2026-10-22 14:07:46 -10:00
J. Nick Koston 470d163f91 [core] Assert the POSIX named_path result only off Windows 2026-10-22 14:07:38 -10:00
J. Nick Koston 887af66470 Merge remote-tracking branch 'origin/store-yaml-core' into store-yaml-firmware 2026-10-22 14:03:10 -10:00
J. Nick Koston 3a7c3ec9a7 [core] Make the Windows named_path test tell the two resolutions apart 2026-10-22 14:02:56 -10:00
J. Nick Koston 4352ea190e Merge remote-tracking branch 'origin/store-yaml-core' into store-yaml-firmware 2026-10-22 13:55:16 -10:00
J. Nick Koston 6a142cafca [core] Document what aliases and links leave out 2026-10-22 13:55:04 -10:00
J. Nick Koston 84c99e25cb Merge remote-tracking branch 'origin/store-yaml-core' into store-yaml-firmware 2026-10-22 13:48:26 -10:00
J. Nick Koston 2b2b55bde7 [core] Keep a literal from taking over its container's YAML alias 2026-10-22 13:48:11 -10:00
J. Nick Koston 6996c9191f Merge remote-tracking branch 'origin/store-yaml-core' into store-yaml-firmware 2026-10-22 13:45:44 -10:00
J. Nick Koston e05dbbc2f5 [core] One discovery result shape on every path; pin the sensitive lambda dump 2026-10-22 13:45:22 -10:00
J. Nick Koston 5a5c6f5c0d Merge remote-tracking branch 'origin/store-yaml-core' into store-yaml-firmware
# Conflicts:
#	tests/unit_tests/test_yaml_util.py
2026-10-22 13:39:26 -10:00
J. Nick Koston 2547475bbd [core] Add the YAML loader helpers store_yaml builds on 2026-10-22 13:38:26 -10:00
J. Nick Koston 7898d04ad2 [store_yaml] Word the unverified-credential error for every case it covers 2026-10-22 13:27:14 -10:00
J. Nick Koston 8f046887d7 [store_yaml] Treat every cv.sensitive key name as a credential, also when a merge or override drops it 2026-10-22 13:22:07 -10:00
J. Nick Koston a702e8f362 [store_yaml] Confirm the send buffer pin before stalling in the backpressure test 2026-10-22 13:17:57 -10:00
J. Nick Koston 1de9fc9c3b [store_yaml] Test multi-chunk recovery under backpressure; read sources as UTF-8 in the guard test 2026-10-22 13:10:17 -10:00
J. Nick Koston 632be43d54 [store_yaml] Name the longest secret first in package descriptions 2026-10-22 13:01:11 -10:00
J. Nick Koston 51332a101a [store_yaml] Name every cv.sensitive key for removed branches and guard the list with a test 2026-10-22 12:58:55 -10:00
J. Nick Koston a4f93bba95 [store_yaml] Name secrets inside package URLs, follow id removals and package-scoped removals, tighten tests 2026-10-22 12:47:07 -10:00
J. Nick Koston 871035de32 [store_yaml] Name credentials in removed branches, limit the package password rule, skip disabled templated includes 2026-10-22 12:34:06 -10:00
J. Nick Koston c73fb52ec2 [store_yaml] Reject only real include tags in a secrets file; cover the lambda branch of cv.sensitive 2026-10-22 12:23:06 -10:00
J. Nick Koston 1eb1cc73da [api] Drop GetYamlResponse.encoding; the blob is always a zstd frame 2026-10-22 12:14:44 -10:00
J. Nick Koston c0b33e26fb [store_yaml] One rule for scalars holding a sensitive value, fold the alias tracing, drop track_yaml_loads 2026-10-22 12:09:29 -10:00
J. Nick Koston bbf3b622f8 [store_yaml] Fail closed on a secret inside a larger value and on a secrets file that includes others 2026-10-22 12:02:47 -10:00
J. Nick Koston 1b303c9686 [store_yaml] Assert the captured package's own include on both platforms 2026-10-22 08:32:01 -10:00
J. Nick Koston 5b6566329d [store_yaml] Swap a quoted integer credential like an unquoted one 2026-10-22 08:22:00 -10:00
J. Nick Koston 68a5925ff9 [store_yaml] Follow the loader's lexical .. on Windows and compare verbatim bytes in the alias test 2026-10-22 08:19:29 -10:00
J. Nick Koston 9fd7fd266b Merge remote-tracking branch 'origin/dev' into store-yaml-firmware
# Conflicts:
#	tests/integration/raw_api_client.py
2026-10-22 08:09:17 -10:00
J. Nick Koston 0b04acfe61 [store_yaml] Skip disabled includes and include cycles when tracing substitution scopes 2026-10-22 08:06:58 -10:00
J. Nick Koston 345f77a671 [store_yaml] Read substitutions defined in inline packages and keep lambdas without literals 2026-10-22 07:08:55 -10:00
J. Nick Koston 1838d53147 [store_yaml] Catch lambda credentials, resolve substitution aliases, and keep disabled includes optional
cv.sensitive now marks a lambda too, so a credential set by one fails
closed instead of shipping in clear. A substitution that aliases another is
followed to its definitions, with a cycle guard. An integer credential
reused through a substitution is swapped at the definition as well. A
missing include behind a static condition: false no longer aborts
discovery, and a package ref from !secret is named, not printed.
2026-10-22 01:18:28 -10:00
J. Nick Koston 18d3a50bcd [store_yaml] Trim the redaction pass and its tests again
The tree pass is a closure of the redaction, collecting includes and
removals as it goes; include scopes propagate each invocation once; the
error paths share one table; the test tables register the secrets file as
discovery would and drop rows that repeat a branch. The symlink note now
measures from the same directory discovery used, so a config reached through
a symlinked directory names its own links.
2026-10-22 01:13:31 -10:00
J. Nick Koston 8c10399320 [store_yaml] Check included files at their include site, swap integer credentials only at their keys, track symlinks below the config directory
An included file's paths are checked at the config location its include
site gives them, with every packages prefix merged away, so a key: option in
an included list file is no longer taken for api.encryption.key. An integer
credential is swapped by the tree pass at its own keys instead of by the
dumper everywhere, so an unrelated 0 stays 0. Directory symlink tracking
skips the config directory and its ancestors.
2026-10-22 00:56:17 -10:00
J. Nick Koston 9d6d996333 [store_yaml] Put a file's own defaults above the caller's chain in nested include scopes
The loader resolves an include's vars, then the file's defaults, then the
caller's chain; the scope analysis now builds the same chain per invocation,
so a literal default in a nested file at a credential location fails closed.
2026-10-22 00:50:37 -10:00
J. Nick Koston 99f6daf607 [store_yaml] Cut comments and docstrings to the house length 2026-10-22 00:48:54 -10:00
J. Nick Koston 75d7d263cf [store_yaml] Inherit caller variables in nested includes, keep secret vars mappings, scan shared texts
A nested include sees the variables its caller received, as the substitution
pass does, with each invocation kept apart. An include whose whole vars
mapping is a !secret is recorded before folding, so scope analysis still
sees its names and treats their values as secret. A text that is an integer
credential in one place and a string in another keeps the substring scan.
2026-10-22 00:45:08 -10:00
J. Nick Koston 6af39d459c [store_yaml] Check removed subtrees, keep zero credentials, and follow directory symlinks
Under a subtree that !remove drops before validation, any key that is a
credential elsewhere in the config is a credential location. An integer
credential of 0 is redacted like any other value; only an empty string is
skipped. Every !include is pointed at its target's captured name, so an
include through a directory symlink recovers, and discovery records the
symlinked directories so the verbatim mode's note says how to recreate them.
2026-10-22 00:36:29 -10:00
J. Nick Koston e5829c7f00 [store_yaml] Trim the component and its tests
Fail-closed and redaction behaviour are unchanged. The redaction tests share
one helper and two parametrized tables, the envelope packer and decoder use
struct directly, the three error paths share one message, the include walk
happens while preparing each tree, the credential check keeps git package
keys out of the component, the git URL masker stays private with only its
pattern exported, the API key check reuses noise.static_encryption_key, and
the integration client reads unknown protobuf fields with the library's own
UnknownFieldSet.
2026-10-22 00:26:02 -10:00
J. Nick Koston 0130af44b5 [store_yaml] Track credential paths by location, scan keys, and reparse files by name
Credential locations come from every path validation marked sensitive, not
from the value-deduplicated map, so domains sharing a password and empty final
values keep theirs; matching allows segments validation inserted, so an action
variable named key is no longer taken for the encryption key. Mapping keys are
scanned for embedded credentials, except the key a value is sensitive at.
Include scopes follow the loader's precedence per include. Files are reparsed
and includes rebased by a path whose directory is resolved but whose name is
kept, so a symlinked file keeps its name while .. follows the link.
2026-10-22 00:05:34 -10:00
J. Nick Koston e6eb127512 [store_yaml] Match validated path shapes and verify references in each include's scope
A credential in an inline package or regrouped by validation (wifi.password
to wifi.networks.password) is still recognised as a credential location.
Substitution references are verified against every definition the file can
see: its include vars, its defaults and the top-level substitutions. Container
secrets inside include vars are folded too, and every URL in a scalar is
checked. The substitution and URL userinfo patterns come from
esphome.expression and esphome.git, the git package keys from packages, and
one walker prepares each tree.
2026-10-21 23:24:59 -10:00
J. Nick Koston cb262fdc64 [store_yaml] Emit every alias of a file and keep credentials out of error paths
Discovery records each name a file is reached by, so two symlinks to one
package both come back in the recovery data. A sensitive value used as a
mapping key is reported by its config path, never by the key itself, and
paths in every message mask segments that are credentials.
2026-10-21 23:09:04 -10:00
J. Nick Koston 12099335ea [store_yaml] Trace substituted credentials to secrets and keep container secrets as references
A substitution under a credential path or inside URL userinfo is followed to
its definition in the captured files, which must itself be a secret or a
verified sensitive value; anything else fails closed. A mapping or list loaded
through !secret is written back as the reference instead of its contents.
2026-10-21 23:04:22 -10:00
J. Nick Koston ecaf8518e6 [store_yaml] Match the quoted skeleton in the recovery integration test 2026-10-21 22:50:26 -10:00
J. Nick Koston 8f39754dd5 [store_yaml] Fail closed on unverifiable credentials and keep more of the source structure
Redaction now rejects a value under a credential path that the validated
config never saw (overridden by a later package, or a git package password
stripped before validation) and a URL carrying credentials, unless it comes
from !secret; include conditions are scanned like vars. Remote source
descriptions mask URL userinfo or name the secret. The secrets skeleton is
written with plain PyYAML so keys like "true" stay strings. Files reached
through a symlink are laid out under the name the YAML used, a document that
is one !include keeps its vars, includes pulled in by !include_dir_* are
rebased to the file they are dumped in, and !literal survives the dump.
2026-10-21 22:48:50 -10:00
J. Nick Koston 81d042dc2c [store_yaml] Drop missing secrets probes in discovery, not in the loader
device-builder tracks the absent secrets.yaml a package's !secret probes so
creating it later triggers a rebuild, so the loader keeps reporting every
path it tries. discover_user_yaml_files now skips paths that do not exist.
2026-10-21 21:50:09 -10:00
J. Nick Koston 1a29eeb52d [store_yaml] Only track YAML files the loader actually opened
A !secret in a package probes a secrets.yaml next to that file before
falling back to the entry's directory. The load listeners fired before the
open, so the missing probe was tracked and include_secrets: true failed the
build trying to read it.
2026-10-21 21:43:32 -10:00
J. Nick Koston e23cd53179 Merge branch 'dev' into store-yaml-firmware 2026-10-21 21:39:47 -10:00
pre-commit-ci-lite[bot] 0bd3738430 apply automatic formatting fixes 2026-10-22 04:52:30 +00:00
J. Nick Koston e558e8416e Merge remote-tracking branch 'origin/dev' into store-yaml-firmware
# Conflicts:
#	tests/integration/raw_api_client.py
2026-10-21 18:50:59 -10:00
J. Nick Koston 2f916194af Merge branch 'dev' into store-yaml-firmware 2026-10-13 20:54:22 -10:00
J. Nick Koston dbb74e6746 Redact sensitive integer values in store_yaml 2026-10-13 20:15:59 -10:00
J. Nick Koston 3adf71ce21 Merge remote-tracking branch 'origin/dev' into store-yaml-firmware 2026-10-13 13:33:29 -10:00
J. Nick Koston 53e7d78156 Simplify store_yaml, trim comments, dedup tests, add DOMAIN 2026-10-12 13:36:02 -10:00
J. Nick Koston 76a086d999 Merge remote-tracking branch 'origin/dev' into store-yaml-firmware 2026-10-12 13:28:35 -10:00
J. Nick Koston 71e55e300c Merge branch 'store-yaml-helpers-core' into store-yaml-firmware 2026-10-12 07:56:25 -10:00
J. Nick Koston f72e256dd3 Merge remote-tracking branch 'origin/dev' into store-yaml-helpers-core
# Conflicts:
#	esphome/yaml_util.py
2026-10-12 07:56:22 -10:00
J. Nick Koston 4e3f72e360 Merge branch 'store-yaml-helpers-core' into store-yaml-firmware
# Conflicts:
#	esphome/components/api/api.proto
#	esphome/components/api/api_connection.cpp
#	esphome/components/api/api_pb2.cpp
#	esphome/components/api/api_pb2.h
#	esphome/components/api/api_pb2_dump.cpp
#	esphome/components/api/api_pb2_service.cpp
#	esphome/components/api/api_pb2_service.h
2026-10-09 07:29:32 -10:00
J. Nick Koston 94630282ee Merge branch 'store-yaml-secret-helpers' into store-yaml-helpers-core
# Conflicts:
#	esphome/components/packages/__init__.py
2026-10-09 07:28:04 -10:00
J. Nick Koston 069d0dd03d Merge remote-tracking branch 'origin/dev' into store-yaml-secret-helpers 2026-10-09 07:27:35 -10:00
J. Nick Koston 69f004793f Merge branch 'store-yaml-helpers-core' into store-yaml-firmware 2026-10-08 07:29:57 -10:00
J. Nick Koston 21183e454d Merge branch 'store-yaml-secret-helpers' into store-yaml-helpers-core 2026-10-08 07:29:57 -10:00
J. Nick Koston 2bc1ced27a [core] Stop the secret reference scan at flow collection punctuation 2026-10-08 07:29:56 -10:00
J. Nick Koston a83a0b88f9 Merge branch 'store-yaml-helpers-core' into store-yaml-firmware 2026-10-08 07:24:43 -10:00
J. Nick Koston d3410103e2 Merge branch 'store-yaml-secret-helpers' into store-yaml-helpers-core 2026-10-08 07:24:43 -10:00
J. Nick Koston 5fb9ac56f9 [core] Track loaded secret names separately and pass nested emitted names outward 2026-10-08 07:24:43 -10:00
J. Nick Koston 6763b5627d [store_yaml] Trim comments and docstrings 2026-10-08 06:36:46 -10:00
J. Nick Koston 2bcaf016b3 [core] Trim the discovery helper comments 2026-10-08 06:36:00 -10:00
J. Nick Koston 62c9015248 Merge branch 'store-yaml-helpers-core' into store-yaml-firmware 2026-10-08 06:36:00 -10:00
J. Nick Koston 55d5d4c12c Merge branch 'store-yaml-secret-helpers' into store-yaml-helpers-core 2026-10-08 06:35:52 -10:00
J. Nick Koston 0cfede51c5 [core] Trim the secret helper comments 2026-10-08 06:35:35 -10:00
J. Nick Koston 73dac9e655 Merge remote-tracking branch 'origin/store-yaml-helpers-core' into store-yaml-firmware 2026-10-08 06:33:39 -10:00
J. Nick Koston c5bb88d866 [core] Match the bad candidate by file name so the test passes on Windows 2026-10-08 11:08:44 -04:00
J. Nick Koston 6d41a7b69a [core] Warn when a secret swap drops a wrapper tag and test the bundle scan warning 2026-10-08 11:08:44 -04:00
J. Nick Koston a231bce3c6 [core] Log discovery load failures once and test package fetch order 2026-10-08 11:08:44 -04:00
J. Nick Koston 3d42a51053 [core] Add YAML secret reference and registration helpers 2026-10-08 11:08:44 -04:00
J. Nick Koston 9f775b95f9 [core] Report incomplete YAML discovery and record remote package sources 2026-10-08 11:08:44 -04:00
J. Nick Koston 1fc0dbbf09 Merge branch 'store-yaml-helpers-core' into store-yaml-firmware 2026-10-07 23:03:30 -10:00
J. Nick Koston 038feaa95a [core] Match the bad candidate by file name so the test passes on Windows 2026-10-07 23:03:28 -10:00
J. Nick Koston f3588da2e6 [store_yaml] Describe what the component embeds in its isolation reason 2026-10-07 22:55:26 -10:00
J. Nick Koston 14d85dd639 Merge branch 'store-yaml-helpers-core' into store-yaml-firmware 2026-10-07 22:55:01 -10:00
J. Nick Koston e1b5804036 [core] Log discovery load failures once and test package fetch order 2026-10-07 22:54:47 -10:00
J. Nick Koston 8d9e9de8eb Merge branch 'store-yaml-secret-helpers' into store-yaml-helpers-core 2026-10-07 22:52:21 -10:00
J. Nick Koston 66374467bf [core] Warn when a secret swap drops a wrapper tag and test the bundle scan warning 2026-10-07 22:51:55 -10:00
J. Nick Koston 5385a35b56 Merge branch 'store-yaml-helpers-core' into store-yaml-firmware 2026-10-07 22:40:38 -10:00
J. Nick Koston 3e812922f3 [core] Report incomplete YAML discovery and record remote package sources 2026-10-07 22:40:31 -10:00
J. Nick Koston 8505074859 [core] Add YAML secret reference and registration helpers 2026-10-07 22:40:09 -10:00
J. Nick Koston 95dac2c3dc [core] Move zstd_module from the api wizard to esphome.helpers 2026-10-07 22:38:42 -10:00
J. Nick Koston 7288aa478d Merge branch 'store-yaml-helpers-core' into store-yaml-firmware 2026-10-07 22:22:50 -10:00
J. Nick Koston e0ae16daaf Merge remote-tracking branch 'origin/dev' into store-yaml-helpers-core 2026-10-07 22:22:14 -10:00
J. Nick Koston dafa2c2729 Merge remote-tracking branch 'origin/store-yaml-helpers-core' into store-yaml-firmware-wt 2026-10-07 13:34:24 -10:00
J. Nick Koston b192a1beff Restore the outer secret collector on exit and bound decode_fields slices
A nested secret_values_registered context hands the collector back to the
outer one. decode_fields asserts on truncated length-delimited and fixed32
fields, and read_frame documents that it returns the oldest unconsumed frame.
2026-10-07 13:34:11 -10:00
J. Nick Koston 3b79d6fcaa [store_yaml] Emit the blob as a flash symbol instead of a component
Codegen emits STORE_YAML_DATA and STORE_YAML_DATA_SIZE the way the wizard
does, so the component, its global pointer and the wait for its setup are
gone and the API reads the blob directly. Record whether a sensitive value
came from !secret once, merge the unencrypted warnings, compute the remote
package list once and trim comments.
2026-10-07 13:32:02 -10:00
J. Nick Koston 56308ebfae Merge remote-tracking branch 'origin/api-progmem-bytes-option' into store-yaml-firmware-wt 2026-10-07 13:27:53 -10:00
J. Nick Koston bd606eb7c5 Merge remote-tracking branch 'origin/store-yaml-helpers-core' into store-yaml-firmware-wt 2026-10-07 13:27:53 -10:00
J. Nick Koston c8fd00e8c4 Simplify the include walk, secret collector, package sources and raw client
force_load_include_files creates its result once and the recursive walker
returns nothing, the never nested secret collector is a single set, package
sources are an ordered dict returned as a tuple, and the raw api client
shares one receive step.
2026-10-07 13:27:39 -10:00
J. Nick Koston fbe43dbca7 Share the dump byte limit and read the progmem option directly 2026-10-07 13:25:28 -10:00
J. Nick Koston d7b5a8e9c8 [store_yaml] Use the shared zstd, progmem and raw api client helpers
GetYamlResponse marks its data (progmem), so every platform streams the
blob straight from flash and the ESP8266 bounce buffer is gone. The zstd
import and the integration test's plaintext client use the shared helpers.
2026-10-07 13:21:05 -10:00
J. Nick Koston bbf1390fea Merge remote-tracking branch 'origin/store-yaml-helpers-core' into store-yaml-firmware-wt 2026-10-07 13:20:01 -10:00
J. Nick Koston 4329314996 Keep real secrets on context exit, log parse failures once, return a copy of package sources
secret_values_registered only removes mappings it still owns, so a real
!secret loaded meanwhile is not dropped. Discovery no longer logs a parse
failure itself, as bundle reports it with the consequence. Note in the
docstring that a wrapper equal to a secret loses its tag.
2026-10-07 13:19:45 -10:00
J. Nick Koston a2116afd97 Merge remote-tracking branch 'origin/api-progmem-bytes-option' into store-yaml-firmware-wt 2026-10-07 13:17:23 -10:00
J. Nick Koston b261578dd3 Merge remote-tracking branch 'origin/store-yaml-helpers-core' into store-yaml-firmware-wt
# Conflicts:
#	requirements.txt
2026-10-07 13:17:23 -10:00
J. Nick Koston c7073eb1ad Keep payloads in the raw api test client
read_frame() returns the next payload of a message type and decode_fields()
splits a payload by field number, for messages the installed aioesphomeapi
does not know yet.
2026-10-07 13:17:00 -10:00
J. Nick Koston bb305bea4d [api] Add a progmem option for bytes fields kept in flash
The generated encode and dump of a pointer_to_buffer bytes field marked
(progmem) copy the data with progmem_memcpy, so ESP8266 can send it from
flash. The device wizard uses it in place of its hand written encoder.
2026-10-07 13:14:52 -10:00
J. Nick Koston 5884ca4d32 [core] Add YAML discovery, secret and zstd helpers
Report unresolved includes and load errors from include discovery, share
the !secret reference scan between bundle and other callers, let callers
register extra secret values for a dump and see which names it emitted,
swap secrets in the !extend, !remove and scalar !include representers,
record the remote sources packages fetch, and move zstd_module out of the
api wizard into esphome.helpers.
2026-10-07 13:06:40 -10:00
J. Nick Koston 6cca15181d Merge remote-tracking branch 'origin/dev' into store-yaml-firmware-wt
# Conflicts:
#	CODEOWNERS
#	esphome/components/api/api.proto
#	esphome/components/api/api_connection.cpp
#	esphome/components/api/api_pb2.cpp
#	esphome/components/api/api_pb2.h
#	esphome/components/api/api_pb2_dump.cpp
#	esphome/components/api/api_pb2_service.cpp
#	esphome/yaml_util.py
2026-10-07 13:03:08 -10:00
J. Nick Koston da6a016291 Merge remote-tracking branch 'upstream/dev' into HEAD 2026-09-17 08:49:32 -05:00
J. Nick Koston 2f3b573752 [store_yaml] Report store_yaml support in DeviceCapabilitiesResponse instead of DeviceInfoResponse 2026-09-14 15:29:10 -05:00
J. Nick Koston e47133171f Merge remote-tracking branch 'origin/dev' into store-yaml-firmware
# Conflicts:
#	esphome/components/api/api.proto
#	esphome/components/api/api_pb2.h
#	esphome/components/api/api_pb2_service.cpp
#	esphome/yaml_util.py
#	tests/integration/conftest.py
#	tests/unit_tests/test_yaml_util.py
2026-09-14 11:07:43 -05:00
J. Nick Koston 5eafd9d2f3 [store_yaml] Cover scalar include secret swap, document secret registry ordering 2026-07-26 10:32:31 -10:00
J. Nick Koston 180414dab4 [store_yaml] Swap secrets in wrapper representers, EsphomeError on cross-anchor paths, branched warning 2026-07-26 08:57:27 -10:00
J. Nick Koston b20e2e0448 [store_yaml] Scan lambda and include payloads, warn on secret overlaps, explicit key gate 2026-07-26 08:14:44 -10:00
J. Nick Koston dbfdd591e8 [store_yaml] Simplify: emission report, unified walker, single scan pass, plain make_unique 2026-07-25 17:25:50 -10:00
J. Nick Koston 21ddd8270f [store_yaml] Scan tree scalars for leaks, guard key swaps, boot race, OOM check 2026-07-25 17:08:20 -10:00
J. Nick Koston 49898359be [store_yaml] Simplify: packages records remote sources, single tree walker, dict-style test packages 2026-07-25 16:46:04 -10:00
J. Nick Koston 8e8aac8fcb [store_yaml] Address review: chunking, re-request no-op, capture gaps, leak scan, tests 2026-07-25 16:22:37 -10:00
J. Nick Koston 66d3816e65 Merge remote-tracking branch 'origin/dev' into store-yaml-firmware-work
# Conflicts:
#	esphome/components/api/api.proto
#	esphome/components/api/api_pb2.cpp
#	esphome/components/api/api_pb2.h
#	esphome/components/api/api_pb2_dump.cpp
#	esphome/yaml_util.py
2026-07-25 15:34:25 -10:00
J. Nick Koston df01cb1029 Merge branch 'dev' into store-yaml-firmware 2026-07-03 21:34:47 -05:00
J. Nick Koston 9080082b45 Trigger re-review 2026-07-03 21:33:00 -05:00
J. Nick Koston 4dbf15ac95 [store_yaml] Record uncaptured includes as an envelope note in both modes 2026-07-03 21:20:53 -05:00
J. Nick Koston 6fdbdcbffe Trigger re-review 2026-07-03 21:10:55 -05:00
J. Nick Koston f00efea65c [store_yaml] Fix stale _gather_files return annotation 2026-07-03 21:01:21 -05:00
J. Nick Koston c625c6b795 [store_yaml] Address review: retry terminal frame, fail on unredactable secrets, note uncaptured includes 2026-07-03 20:57:54 -05:00
J. Nick Koston 8f2c09e3c8 [store_yaml] Simplify redaction pipeline, fix Windows newline test failure 2026-07-03 20:40:14 -05:00
J. Nick Koston a25b7a806b [store_yaml] Recover secrets as !secret references using cv.sensitive 2026-07-03 20:31:32 -05:00
J. Nick Koston bd297f08a8 [store_yaml] Simplify: lazy YAML discovery in to_code, zero-copy streaming, shared envelope decoder 2026-07-03 19:38:41 -05:00
J. Nick Koston e4bb7c93a2 [store_yaml] Add coverage for gated YAML discovery in load_config 2026-07-03 19:23:07 -05:00
J. Nick Koston f3ac60b45c [store_yaml] Gate YAML discovery on component presence, fail on unreadable files, warn on unresolved includes 2026-07-03 19:17:04 -05:00
J. Nick Koston d6019ca2c0 Merge branch 'dev' into store-yaml-firmware 2026-07-03 19:04:12 -05:00
J. Nick Koston 5f155c90b7 Merge branch 'dev' into store-yaml-firmware 2026-06-09 20:51:44 -05:00
J. Nick Koston a8d5ffc141 Merge branch 'dev' into store-yaml-firmware 2026-05-26 09:17:39 -05:00
J. Nick Koston a9ec101631 [store_yaml] Use memcpy_P on ESP8266, std::memcpy elsewhere
Byte-by-byte `progmem_read_byte` for a 512-byte chunk does 512 aligned
32-bit flash reads + shifts on ESP8266. Switching to `memcpy_P` lets the
SDK do bulk aligned-flash copies. Other platforms get a plain
`std::memcpy` since their `PROGMEM` is a no-op and the blob lives in
normal address space.

`memcpy_P` is only available on ESP8266 (via `<pgmspace.h>`), so the
implementation is platform-gated; both paths boil down to a one-call
copy.
2026-05-15 22:36:15 -07:00
J. Nick Koston cd2c1014f3 [store_yaml] Address review: drop esp32-ard test, force on data field
- Remove `test.esp32-ard.yaml`; the esp32 platform is IDF-only for new
  component tests.
- `(force) = true` on `GetYamlResponse.data` so the field is always
  emitted on the wire (matches the camera streaming pattern). `done`
  stays unforced — it's `false` on every chunk except the last, and
  default-value elision saves ~2 bytes per chunk for the firmware to
  TX.
- `total_size` and `encoding` stay first-chunk-only; the client caches
  them. Firmware bandwidth is expensive, client RAM is not.
2026-05-15 10:33:44 -07:00
J. Nick Koston 41081b7278 [store_yaml] Address review: ConfigType typing and drop redundant priority override
- Annotate `_final_validate` and `to_code` with `ConfigType` parameter and
  return types.
- Drop the `get_setup_priority()` override on `StoreYamlComponent`; the
  default `Component::get_setup_priority()` already returns
  `setup_priority::DATA`, so the override was a no-op.
2026-05-15 10:29:05 -07:00
J. Nick Koston f06e96685b [store_yaml] Refactor YAML discovery to share bundle.py's approach
The previous implementation extended `track_yaml_loads` across the entire
validation pass to catch deferred `!include` and package loads, but that also
captured framework YAML loaded internally by component validators (e.g.
LVGL's `hello_world.yaml`) and produced spurious "unresolved substitution"
warnings during the pre-validation force-load. bundle.py already had the
right pattern: a fresh post-validation re-parse plus `force_load_include_files`.

- Lift the discovery into `yaml_util.discover_user_yaml_files` and have both
  `bundle.py` and `config.py` use it (DRY).
- Capture `secrets.yaml` / `secrets.yml` by the *un-resolved* listener fname
  so a `secrets.yaml` symlinked to a non-secrets-named target is still
  flagged for redaction.
- Add a `warn_on_unresolved` flag to `force_load_include_files` so the
  discovery path (where substitutions haven't run) logs at debug instead of
  warning.
- Reject `store_yaml` configs with an unencrypted API via
  `FINAL_VALIDATE_SCHEMA`; an explicit `allow_unencrypted: true` opt-out
  keeps lab setups working (renamed from `allow_unencrypted_api` so the
  integration-test harness's naive `api:` string replacement doesn't
  clobber it).
- Annotate `store_yaml_chunk_buf` with the
  `cppcoreguidelines-avoid-non-const-global-variables` suppression that
  matches other intentional API-side globals.
- Update unit tests to exercise the new `DiscoveredYamlFiles` shape plus a
  symlink-secrets case.
2026-05-15 10:21:00 -07:00
J. Nick Koston 6493fdaba1 [store_yaml] Add end-to-end integration test for native-API recovery
Compiles a host build with `store_yaml`, drives a raw plaintext API socket
(the released aioesphomeapi does not yet know about GetYamlRequest /
GetYamlResponse and would silently drop the streamed bytes as "unknown
message type"), sends GetYamlRequest, accumulates the streamed
GetYamlResponse chunks until done=true, zstd-decompresses, and verifies
both the envelope structure and that the fixture's distinctive markers
(`store-yaml-test`, `store_yaml:`) round-trip back through the recovery
blob.
2026-05-15 10:20:45 -07:00
J. Nick Koston 3d77e3f5dd [store_yaml] Address review: capability bit, dump suppression, paths, unit tests
- Advertise `has_store_yaml` in DeviceInfoResponse so recovery tooling can
  detect support without timing out against firmware built without
  USE_STORE_YAML.
- Suppress GetYamlResponse from the proto dump path. Every chunk would
  otherwise log embedded configuration (including opted-in secrets) on
  builds with HAS_PROTO_MESSAGE_DUMP, and bloat logs during recovery.
- Preserve the include graph for files outside the project root: use
  `os.path.relpath` instead of just the basename so e.g. two
  `../common.yaml` siblings don't collide on recovery.
- Keep `track_yaml_loads` open across `validate_config` so files loaded
  by remote packages and substitution-resolved includes are captured.
- Add focused unit tests for `_gather_files` (redaction, secrets.yml,
  opt-in, dedupe, external-path handling, missing sources) and
  `_pack_envelope` (round-trip, UTF-8 paths, overlong-path guard).
- Make the test_bundle assertion case-insensitive.
2026-05-15 10:20:45 -07:00
J. Nick Koston 7f5de80f81 [store_yaml] Drop dead re-resolve and duplicated SECRETS_FILES check
`track_yaml_loads` already returns resolved Path objects, so re-wrapping
each entry in `Path(...).resolve()` was a no-op. The OR'd
`Path(path).name in SECRETS_FILES` branch always evaluated the same as
the first check on the resolved name; removing it. Document the symlink
edge case the resolver swallows so a future change to the loader can
revisit it.
2026-05-15 10:20:45 -07:00
J. Nick Koston 14628ab3a5 [store_yaml] Add return type annotation on _import_zstd 2026-05-15 10:20:45 -07:00
J. Nick Koston 150a65de8c [store_yaml] Force-load deferred includes; cover secrets.yml
- Promote `_force_load_include_files` from bundle.py to yaml_util.py as
  `force_load_include_files`. bundle.py and the new caller now share it.
- Call it inside the `track_yaml_loads` block in config.py so deferred
  `!include` values, packages, and similar are loaded while the listener
  is active. Without this, `CORE.data["yaml_sources"]` only contained the
  entry YAML, and store_yaml shipped an incomplete recovery blob.
- Treat both `secrets.yaml` and `secrets.yml` as secrets (matches
  `esphome.const.SECRETS_FILES`); also check the original (non-resolved)
  filename to catch a `secrets.yaml` symlinked to an unrelated target.
- Update bundle tests for the new import path and message wording.
2026-05-15 10:20:45 -07:00
J. Nick Koston 783afaeaf2 [store_yaml] Streaming: advance pos only on successful send
Drop the pre-advance + rewind-on-failure dance in try_send_store_yaml_; match
the camera streaming pattern (advance after send_message succeeds, retry the
same chunk on WOULD_BLOCK). Also remove the unreachable `|| pos == 0` branch
in the while condition — on_get_yaml_request short-circuits the zero-size
path before this function is ever called.
2026-05-15 10:20:04 -07:00
J. Nick Koston 59e8242756 [store_yaml] Address review: combined dump_config, encapsulate PROGMEM read
- Coalesce dump_config into a single ESP_LOGCONFIG call.
- Replace the raw get_data() accessor with a read_chunk() helper that copies
  bytes via progmem_read_byte, so callers cannot accidentally dereference the
  PROGMEM pointer directly on ESP8266.
- Update the API streaming handler to use read_chunk().
2026-05-15 10:20:04 -07:00
J. Nick Koston d0510364c9 [store_yaml] Embed user YAML in firmware for recovery
Adds a new opt-in component that compresses the on-disk YAML files with zstd
at codegen time and stores them in PROGMEM. The native API exposes a chunked
GetYaml RPC so a lost configuration can be retrieved from a running device.
Decompression happens client-side; no decompressor is shipped on-device.
2026-05-15 10:20:04 -07:00
357 changed files with 5574 additions and 8750 deletions

No files matched your search

-215
View File
@@ -1,215 +0,0 @@
---
name: automations
description: Write ESPHome automation building blocks - triggers (on_* keys), actions and conditions. Use when adding or changing an on_* trigger, an action such as my_component.do_something, a condition, a callback registration method (add_on_*_callback), or code that calls build_callback_automation, build_automation, register_*_action or register_*_condition.
---
# Automations: triggers, actions and conditions
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do
something), and **Conditions** (check if something is true). The helpers named here are documented in
their docstrings in `esphome/automation.py`.
## Triggers: callback method (preferred)
Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by
using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID`
in the schema.
**Python:**
```python
from esphome import automation
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(MyComponent),
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
for conf in config.get(CONF_ON_STATE, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [(bool, "x")], conf
)
```
`build_callback_automation` arguments: `parent`, `callback_method` (C++ method name), `args` (template args
as `[(type, name)]` tuples), `config`, and optional `forwarder` (defaults to `TriggerForwarder<Ts...>`).
For boolean filtering (e.g. `on_press`/`on_release`), use built-in forwarders with `args=[]`:
```python
for conf_key, forwarder in (
(CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
(CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=forwarder
)
```
When the callback's parameters differ from the automation's arguments, or the trigger should only fire for
some values, pass `params`, `forward` and `when`; the helper then generates a capture-less lambda (stored
inline, nothing allocated). Name the parent with `automation.parent_ref(var)` and build `forward` from
`MockObj` calls, not f-strings of C++:
```python
parent = automation.parent_ref(var)
index = cg.RawExpression("index")
await automation.build_callback_automation(
var,
"add_on_state_callback",
[(cg.StringRef, "x"), (cg.size_t, "i")],
conf,
params=[(cg.size_t, "index")],
forward=[cg.StringRef(parent.option_at(index)), index],
)
```
`build_parent_callback_automation`, `build_trigger_callback`, `build_callback_automations` and
`build_trigger_automations` cover the other shapes; see their docstrings in `esphome/automation.py`.
**C++: no trigger class needed.** The callback registration method must be templatized to accept both
`std::function` and lightweight forwarder structs (which avoid heap allocation):
```cpp
class MyComponent : public Component {
public:
// Must be a template -- accepts both std::function and pointer-sized forwarder structs
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
protected:
// Use CallbackManager when callbacks are always registered (e.g. core components)
CallbackManager<void(bool)> state_callback_;
// Use LazyCallbackManager when callbacks are often not registered -- saves 8 bytes
// (nullptr vs empty std::vector) per instance when no callbacks are added
// LazyCallbackManager<void(bool)> state_callback_;
};
```
Never type a registration method's parameter as `std::function`; that forces a heap allocation for every
forwarder struct:
```cpp
// Bad
void add_on_state_callback(std::function<void(bool)> &&callback) {
this->state_callback_.add(std::move(callback));
}
```
| Type | Idle overhead (32-bit) | When to use |
|------|----------------------|-------------|
| `CallbackManager<void(Ts...)>` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered |
| `LazyCallbackManager<void(Ts...)>` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) |
Both live in `esphome/core/helpers.h` and accept `std::function`, lambdas and forwarder structs through
their templatized `add()` method. `LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first
callback is added. Prefer it for entity-level callbacks where most instances have no subscribers.
## Triggers: Trigger class method
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state
beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic).
**Python:**
```python
TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template())
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)}
),
}
)
async def to_code(config):
for conf in config.get(CONF_ON_TURN_ON, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
```
**C++:**
```cpp
class TurnOnTrigger : public Trigger<> {
public:
explicit TurnOnTrigger(MyComponent *parent) : last_on_{false} {
parent->add_on_state_callback([this](bool state) {
if (state && !this->last_on_)
this->trigger();
this->last_on_ = state;
});
}
protected:
bool last_on_;
};
```
## Actions
```cpp
template<typename... Ts> class MyAction : public Action<Ts...> {
public:
explicit MyAction(MyComponent *parent) : parent_(parent) {}
void play(const Ts &...) override { this->parent_->do_something(); }
protected:
MyComponent *parent_;
};
```
Register it without writing a builder:
```python
automation.register_simple_action(
"my_component.do_something", MyAction, schema, synchronous=True
)
```
The constructor receives the object named by `config[CONF_ID]`. Use `register_bare_action` for a
no-argument constructor, `register_parented_action` for a class deriving from `Parented<T>`, and the
`@automation.register_action(...)` decorator only when the builder must also set fields.
Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use
`synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`)
or store trigger arguments for later use.
**Actions that only forward templatable values to their parent need no C++ class.** Register them with
`register_apply_action`; do not write a `TEMPLATABLE_VALUE` class or a builder for this shape.
```python
automation.register_apply_action(
"my_component.set_gains",
schema,
automation.ApplyField(CONF_KP, "set_kp", cg.float_),
automation.ApplyField(CONF_KI, "set_ki", cg.float_),
)
```
The `ApplyField`, `ApplyCall` and `register_apply_action` docstrings in `esphome/automation.py` cover the
rest; `cover.control` and `cover.template.publish` are in-tree examples. An action whose input is a byte
payload passes `automation.ApplyBytes(CONF_DATA, "send({})")` in the same call; a constant payload stays in
flash and a lambda's vector is forwarded as data and size. `TEMPLATABLE_VALUE` with `cg.templatable` stays
for actions whose `play()` has real logic beyond forwarding values.
## Conditions
```cpp
template<typename... Ts> class MyCondition : public Condition<Ts...> {
public:
explicit MyCondition(MyComponent *parent) : parent_(parent) {}
bool check(const Ts &...) override { return this->parent_->is_active(); }
protected:
MyComponent *parent_;
};
```
Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`;
`register_bare_condition`, `register_parented_condition` and the decorator follow the action rules.
**Conditions that only test their parent need no C++ class either.** Register them with
`register_apply_condition`; the expression is applied to the parent, and an `ApplyCall` compares against
config values.
```python
automation.register_apply_condition("my_component.is_active", schema, "is_active()")
automation.register_apply_condition(
"my_component.state_is",
schema,
automation.ApplyCall("state == {}", ((CONF_STATE, cg.bool_),)),
)
```
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
-68
View File
@@ -1,68 +0,0 @@
---
name: breaking-changes
description: Decide whether an ESPHome change breaks public API and handle it - what counts as public C++ and Python API, the deprecation window, ESPDEPRECATED for C++, cv.rename_key and deprecation warnings for config keys, and the migration notes a PR needs. Use when removing, renaming or changing the signature of a config option, a public C++ member or a core Python helper.
---
# Public API and breaking changes
Full policy: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes
## What is public
- **C++ in components:** only features documented at [esphome.io](https://esphome.io) are public API.
Undocumented `public` members are internal.
- **C++ core and base classes** (`esphome/core/`, `Component`, `Sensor`, etc.): all `public` members are
public API.
- **Components with global accessors** (`global_api_server`, etc.): all `public` members are public API
(except config setters).
- **Python:** all documented configuration options are public API. Python code in `esphome/core/` that
existing core components use is stable API. Other Python code is internal unless explicitly documented
for external component use.
- Changing the signature of a lambda that code generation injects is not a breaking change.
## Policy
- Aim for a **6-month deprecation window** when possible.
- Clean breaks are allowed for signature changes, deep refactorings and resource constraints.
- The migration path goes in the PR description (it generates the release notes).
- A blog post is required for core/base class changes or significant architectural changes.
Checklist:
- [ ] Clear justification (RAM/flash savings, architectural improvement)
- [ ] Explored non-breaking alternatives
- [ ] Added deprecation warnings if possible (`ESPDEPRECATED` for C++)
- [ ] Documented migration path in the PR description with before/after examples
- [ ] Updated all internal usage and esphome.io
- [ ] Tested backward compatibility during the deprecation period
## Deprecating C++
```cpp
// Remove before 2026.6.0
ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0")
void old_method() { this->new_method(); }
```
## Deprecating config keys
For a renamed key, use the shared `cv.rename_key` validator with `removed_in` (and `component` for
context); it warns and migrates automatically:
```python
CONFIG_SCHEMA = cv.All(
cv.rename_key(
CONF_OLD_KEY, CONF_NEW_KEY, removed_in="2026.6.0", component="my_component"
),
cv.Schema({...}),
)
```
For other deprecations, warn manually during validation:
```python
# Remove before 2026.6.0
if CONF_OLD_KEY in config:
_LOGGER.warning(
f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0"
)
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
```
Cover the old syntax with a `validate.*.yaml` test (see the `component-tests` skill), and keep the language
schema correct (see the `config-validation` skill).
+5 -11
View File
@@ -9,13 +9,11 @@ ESPHome parses YAML into C++ firmware for memory-constrained microcontrollers
(ESP32, ESP8266, RP2040, LibreTiny). Review changes with that in mind: RAM and
flash are scarce, and code runs unattended for months.
`AGENTS.md` in the repository root is the contributor guide, and the skills it
lists (`component-codegen`, `config-validation`, `automations`,
`embedded-memory`, `component-tests`, `breaking-changes`) hold the detail. They
are the authority when they disagree with this summary. The developer
documentation at https://developers.esphome.io explains the component lifecycle
and the reasoning behind these rules. This skill lists the concrete things worth
flagging in a review; read the matching skill for the detail behind any item.
`AGENTS.md` in the repository root is the full contributor guide and the
authority when it disagrees with this summary. The developer documentation at
https://developers.esphome.io explains the component lifecycle and the reasoning
behind these rules. This skill lists the concrete things worth flagging in a
review; read `AGENTS.md` for the detail behind any item.
Only raise findings that the diff actually introduces or changes. Do not ask for
drive-by cleanup of pre-existing code the PR did not touch.
@@ -78,10 +76,6 @@ performance nit, because it fragments a small shared heap. Flag:
`if (blah := config.get(CONF_BLAH)) is not None:`.
- Reuse existing validators from `config_validation.py` (`cv.rename_key`,
`cv.has_exactly_one_key`, etc.) via `cv.All(...)` instead of hand-rolling.
- A new or changed validator function that wraps, chooses or builds a schema
must be decorated with `@schema_extractor(...)` and handle `SCHEMA_EXTRACT`,
or `script/build_language_schema.py` silently drops the keys behind it (see
the `config-validation` skill).
- `esphome/const.py` is frozen: no new `CONF_` constants there. Define them in
the component's own `.py`, or in `esphome/components/const/__init__.py` when
shared. The same constant defined in three or more component files fails CI.
-240
View File
@@ -1,240 +0,0 @@
---
name: component-codegen
description: Write the Python side of an ESPHome component - __init__.py and platform files (sensor.py, switch.py, ...), DOMAIN and other metadata, to_code, hub child entities with sub_sensors and friends, pins and referenced components, hub-forwarding entities (ApplyButton, ApplySwitch), uart/i2c/spi constructor helpers, and CORE.data state. Use when creating a component or changing its code generation.
---
# Component code generation
Read https://developers.esphome.io/architecture/components/ for the component lifecycle and how a
component is registered. Schema rules and schema extraction are in the `config-validation` skill;
automations (`on_*`, actions, conditions) in the `automations` skill.
## Files
```
components/[component_name]/
├── __init__.py # Component configuration schema and code generation
├── [component].h # C++ header file (if needed)
├── [component].cpp # C++ implementation (if needed)
└── [platform]/ # Platform-specific implementations
├── __init__.py # Platform-specific configuration
├── [platform].h # Platform C++ header
└── [platform].cpp # Platform C++ implementation
```
## Metadata
- `DOMAIN`: required in every component's `__init__.py`, set to the component's own name as a plain string
(e.g. `DOMAIN = "my_component"`). Other code refers to the component through it, e.g.
`from esphome.components.my_component import DOMAIN`. Keep it in alphabetical order with the other
metadata constants. CI (`lint_component_domain`) fails without it.
- `DEPENDENCIES`: list of required components.
- `AUTO_LOAD`: components to automatically load. Not a way to put entities under the component's own key;
see "Entities of a hub" below.
- `CONFLICTS_WITH`: incompatible components.
- `CODEOWNERS`: GitHub usernames responsible for maintenance.
- `MULTI_CONF`: whether multiple instances are allowed.
## Basic pattern
```python
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_KEY, CONF_ID
DOMAIN = "my_component"
CONF_PARAM = "param" # A constant that does not yet exist in esphome/const.py
my_component_ns = cg.esphome_ns.namespace("my_component")
MyComponent = my_component_ns.class_("MyComponent", cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(MyComponent),
cv.Required(CONF_KEY): cv.string,
cv.Optional(CONF_PARAM, default=42): cv.int_,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_key(config[CONF_KEY]))
cg.add(var.set_param(config[CONF_PARAM]))
```
```cpp
namespace esphome::my_component {
class MyComponent : public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
void set_key(const char *key) { this->key_ = StringRef(key); }
void set_param(int param) { this->param_ = param; }
protected:
StringRef key_;
int param_{0};
};
} // namespace esphome::my_component
```
When the setter takes the value unchanged, use `cg.add_optional(var.set_blah, config.get(CONF_BLAH))`; it
is not awaited, and it skips only `None`, so `0` and `False` are still set. Required, invariant
dependencies are constructor arguments to `cg.new_Pvariable()`, not setters.
## Entity platforms
**Sensor:**
```python
from esphome.components import sensor
CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(
cv.polling_component_schema("60s")
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
```
**Binary sensor:**
```python
from esphome.components import binary_sensor
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({...})
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
```
**Switch:**
```python
from esphome.components import switch
CONFIG_SCHEMA = switch.switch_schema().extend({...})
async def to_code(config):
var = await switch.new_switch(config)
```
## Entities of a hub
A new component's entities go in platform files (`sensor.py`, `binary_sensor.py`, ...) that take the hub's
id, never under the hub's own key with their platforms added to `AUTO_LOAD` to make that work. Auto loading
a platform because the C++ uses its type is fine. See
https://developers.esphome.io/architecture/components/#exposing-multiple-entities.
```yaml
# Bad - entities under the hub's key
my_hub:
connected:
name: Connected
# Good - the hub, then one platform entry that holds its entities
my_hub:
id: hub1
binary_sensor:
- platform: my_hub
my_hub_id: hub1 # optional with a single hub
connected:
name: Connected
```
**Optional child entities of a hub:** bind the config once with `sensor.sub_sensors(config)` (or
`sub_binary_sensors`, `sub_text_sensors`, `sub_buttons`, `sub_switches`, `sub_numbers`, `sub_selects` in
their domains), adding `parent=hub` for entities that derive from `Parented<T>`, then make one call per
key, even when there is only one. A call creates the entity only when its key is configured, passes it to
the setter and returns it (or `None`); extra arguments such as `min_value` or `options` go on the call.
Always name the setter explicitly on the object that owns it, never with `getattr` and an f-string, and
keep that variable short (`var` for the component itself, `hub` for one fetched with `cg.get_variable`) so
the calls fit on one line. Loops whose setter also takes an index, such as `set_gate_threshold(x, n)`, stay
as they are.
```python
# my_hub/sensor.py
async def to_code(config):
hub = await cg.get_variable(config[CONF_MY_HUB_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, hub.set_temperature_sensor)
await sensors(CONF_HUMIDITY, hub.set_humidity_sensor)
# my_hub/button.py
async def to_code(config):
hub = await cg.get_variable(config[CONF_MY_HUB_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_RESTART, hub.set_restart_button)
```
**Hub forwarding entities need no C++ class:** a button, switch, number or select that only calls its hub
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`.
**Entities that each watch a register field:** one class taking register, shift, mask and match from a
Python tuple table, in typed `StaticVector`s sized by `cg.slot_counter`; not a template per bit, not a
virtual listener base (a second vptr per entity).
## Other helpers
- **Pins and referenced components:** set them with `await cg.add_pin(var.set_cs_pin, config[CONF_CS_PIN])`
and `await cg.add_variable(var.set_bus, config[CONF_BUS_ID])`; both skip a `None` value, so pass
`config.get(KEY)` for an optional key, and return what they created. Use `cg.register_parented` for
`set_parent`.
- `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`) constructs, registers and
attaches in one call.
- `display.add_writer(var, config)` sets the lambda.
- `MyEnum.options("on", prefix="MY_ENUM_")` builds an enum dict.
- `button.restart_button_schema(cls)` (and `factory_reset_`, `query_`) are button presets.
- `fv.get_declaring_config(id)` finds a hub's config in final validation.
## State during code generation
Use `CORE.data` for component state that needs to persist during configuration generation, namespaced
under the component's `DOMAIN` and held in a `@dataclass`. Avoid module-level mutable globals: they
persist between compilation runs if the host process (e.g. device-builder) doesn't fork/exec, whereas
`CORE.data` is cleared between runs.
```python
from dataclasses import dataclass, field
from esphome.core import CORE
DOMAIN = "my_component"
@dataclass
class MyComponentData:
feature_enabled: bool = False
item_count: int = 0
items: list[str] = field(default_factory=list)
def _get_data() -> MyComponentData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = MyComponentData()
return CORE.data[DOMAIN]
def request_feature() -> None:
_get_data().feature_enabled = True
```
Don't use flat keys such as `CORE.data["my_component_feature"] = True`. Some components use `TypedDict`
instead of a dataclass; both are acceptable.
-101
View File
@@ -1,101 +0,0 @@
---
name: component-tests
description: Write and run tests for ESPHome components - YAML build tests (test.*.yaml, validate.*.yaml, common.yaml) under tests/components, shared bus packages and CI test grouping, C++ gtest unit tests, and host integration tests including the uart_mock fixture. Use when adding or changing files under tests/, or when a code change needs test coverage.
---
# Component tests
## C++ unit tests
gtests live in `tests/components/<component>/*.cpp` and run with `script/cpp_unit_test.py <component>`; see
`tests/components/README.md`. Set the loop clock in a test with `LoopBlockingGuard{nullptr, nullptr,
now_ms}`; the runner resets it before every test.
## Integration tests
`tests/integration/` runs real firmware on the host. Serial components prove timeouts and recovery with the
`uart_mock` fixture (`tests/integration/test_uart_mock_*.py`). Its `responses` match on content every time
they see it; to answer a repeated request differently, use an `on_tx` lambda with a static counter and
`inject_to_rx_buffer` (see `uart_mock_ld2412_requery_after_giveup.yaml`).
## YAML build tests
```
tests/
├── test_build_components/
│ └── common/ # Shared bus packages (uart, i2c, spi, etc.)
│ ├── uart/ # UART at default baud rate
│ ├── uart_115200/ # UART at 115200 baud
│ ├── i2c/ # I2C bus
│ └── spi/ # SPI bus
└── components/[component]/
├── common.yaml # Component-only config (no bus definitions)
├── test.esp32-idf.yaml # config + compile
├── test.esp8266-ard.yaml # config + compile
├── test-variant.esp32-idf.yaml # variant test, config + compile
├── validate.esp32-idf.yaml # config-only (never compiled)
└── validate-legacy.esp32-idf.yaml # config-only variant
```
Run them with `script/test_build_components`. Use `-c <component>` to test specific components and
`-t <target>` for specific platforms.
- **Keep build-test variants few:** one `test.<platform>.yaml` per platform the code actually differs on.
A variant per board for platform-identical code only adds CI time.
- **Config-only test files (`validate.*.yaml`):** use this prefix when a YAML file only needs to exercise
schema/validation paths and does not need to be compiled. CI runs `validate.*.yaml` files with
`esphome config` only and skips them during compile. The grammar mirrors `test.*.yaml`:
- `validate.<platform>.yaml`: base config-only test
- `validate-<variant>.<platform>.yaml`: config-only variant
Use this for deprecated-syntax migration tests, schema edge cases, or platform-specific validation
branches where building firmware adds no signal. A component may have any mix of `test.*.yaml` and
`validate.*.yaml` files. Validate files never participate in bus grouping; each one runs as its own
`esphome config` invocation.
When a PR's only edits to a component are `validate.*.yaml` files (no source changes, no `test.*.yaml`
changes, and the component isn't pulled in as a dependency of another changed component), CI skips the
compile stage for that component and only runs config validation. This is decided in
`script/determine-jobs.py` via `_component_change_is_validate_only` and surfaced as the
`validate_only_components` output that the `test-build-components-split` job consumes.
## Grouping with packages
Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define
buses (uart, i2c, spi, modbus) directly in test YAML files**; always use packages from
`test_build_components/common/`.
All includes in test files must go through dict-style `packages:` so that batch grouping works; the
grouping scripts only understand dict-style packages. Never use list-style packages
(`packages: [- !include ...]`) or top-level merge keys (`<<: !include common.yaml`). Bus packages are keyed
by the bus name; the component's `common.yaml` is keyed by the component name (e.g.
`cst328: !include common.yaml`):
```yaml
# test.esp32-idf.yaml - everything included via named packages
packages:
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
my_component: !include common.yaml
```
```yaml
# common.yaml - component config only, NO bus definitions
my_component:
id: my_instance
sensor:
- platform: my_component
name: My Sensor
```
Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing
CI build time. `!extend` and `!remove` in a `test.*.yaml` also make the component ungroupable; add a
standalone entity with its own id instead.
A test that needs a shared component in a particular state (`esp32_ble` security settings, an option that
PSRAM rejects) cannot be grouped: the merged block wins and the whole batch fails. Add the component to
`ISOLATED_COMPONENTS` in `script/analyze_component_buses.py`, or move the option to a `test-<variant>` file.
To verify that all components can be tested together without ID conflicts or configuration issues:
```bash
./script/test_component_grouping.py -e config --all
```
This tests all components in a single build to catch conflicts that might not appear when testing
components individually. Use `-e config` for fast configuration validation, or `-e compile` for full
compilation testing.
-124
View File
@@ -1,124 +0,0 @@
---
name: config-validation
description: Write or change ESPHome configuration validation - CONFIG_SCHEMA, cv.Schema, cv.typed_schema, custom validator functions, config keys and CONF_ constants, renamed or deprecated keys, and the language schema that script/build_language_schema.py extracts from them. Use whenever a change touches a component's schema or a validator in config_validation.py, automation.py or a component's .py files.
---
# Configuration validation
ESPHome validates YAML with `voluptuous` schemas built from `esphome/config_validation.py` (`cv`). The
same schemas are also walked by `script/build_language_schema.py`, which produces the JSON language schema
that editors and the dashboard use for completion and checking. A validation change is only finished when
both still work.
## Writing schemas
- **Type hints:** validators are typed like any other function: `def validate_x(config: ConfigType) ->
ConfigType:`, with `ConfigType` imported from `esphome.types`.
- **One schema per model:** when a component's options depend on a `model` key, use
`cv.typed_schema({...}, key=CONF_MODEL, default_type=...)` with each model's own keys and defaults, not
one schema plus a validator that rejects keys and fills defaults afterwards.
- **Reuse existing validators:** before writing a custom validator, check for an existing one in
`config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components.
For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and
reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See
how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`.
- **Common validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0,
max=100)`, `cv.positive_int`, `cv.percentage`.
- **Complex validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
- **Platform-specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`,
`cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`.
- **Framework-specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`. On the ESP32, Arduino is
built as a component of ESP-IDF, so an ESP-IDF API, sdkconfig option or version floor is there on both
frameworks: gate on `esp32.require_idf_version(cv.Version(5, 5, 3), "psram_dma")`, which checks the
platform and the ESP-IDF under either framework, as `spi`, `usb_host` and `wifi` do. Reserve
`cv.only_with_framework("esp-idf")` for a build that has been tried on Arduino and fails there, and write
the actual failure next to it (`sendspin`: arduino-esp32 below ESP-IDF 6.0 bundles a second libsodium,
and from 6.0 sendspin-cpp's enumerators collide with `Arduino.h` macros such as `DISPLAY`).
- **Schema extensions:**
```python
CONFIG_SCHEMA = cv.Schema({ ... })
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(i2c.i2c_device_schema(0x48))
.extend(spi.spi_device_schema(cs_pin_required=True))
```
- **Constants:** `esphome/const.py` is frozen; do not add new `CONF_` constants there. Define a
component-local constant in the component's own `.py`; for a constant shared by multiple components, add
it to `esphome/components/const/__init__.py`. CI (`lint_constants_usage`) fails if the same constant is
defined in three or more component files. Constants used in core files (those not under
`esphome/components`) may be added to `esphome/const.py` but will require adjustment to the CI check.
- **Renamed or removed keys:** see the `breaking-changes` skill for `cv.rename_key` and manual
deprecation warnings.
- **Config-only tests:** schema edge cases and deprecated-syntax migrations go in `validate.*.yaml` test
files (see the `component-tests` skill).
## Schema extraction
`script/build_language_schema.py` sets `esphome.schema_extractors.EnableSchemaExtraction = True` before it
imports any component, then walks every `CONFIG_SCHEMA` (and platform schema) recursively. It understands
`cv.Schema`, `cv.All`, `cv.Any`, dicts of `cv.Required` / `cv.Optional` keys, `.extend()`, `cv.ensure_list`,
`cv.typed_schema`, registries and the known `cv` validators. **A plain function it cannot see into**: a
custom validator that builds or picks a schema at validation time hides every key behind it, and the
dumper silently emits nothing for them. CI only runs the script with `--check`, which does not compare the
output, so a lost schema is not caught.
When a validator function wraps, chooses or builds a schema (rather than checking a single value), make it
visible to the dumper:
1. Decorate it with `@schema_extractor("<type>")` from `esphome.schema_extractors`.
2. At the top of the function, return the data for that type when it is called with `SCHEMA_EXTRACT`
instead of a config value. Compare with `is` (`value is SCHEMA_EXTRACT`); older code uses `==`.
```python
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@schema_extractor("schema")
def _outgoing_connection_schema(config: ConfigType | None) -> ConfigType:
if config is SCHEMA_EXTRACT:
return _OUTGOING_CONNECTION_SCHEMA
if config is None:
config = {}
return _OUTGOING_CONNECTION_SCHEMA(config)
```
The type names the dumper accepts, and what `SCHEMA_EXTRACT` must return for each, are in the
`hidden_schemas` branch of `convert()` in `script/build_language_schema.py`. The common ones:
| Type | Return for `SCHEMA_EXTRACT` | Example |
|------|-----------------------------|---------|
| `"schema"` | the schema (or a representative one) to walk | `api` `_outgoing_connection_schema`, `restore_state_schema` in `esphome/core/entity_helpers.py`, `model_schema_extractor` in `esphome/components/mipi/__init__.py` |
| `"enum"` | the dict of options | `sensor_entity_category` in `esphome/components/sensor/__init__.py` |
| `"one_of"` | the list of options | `cv.one_of` |
| `"maybe"` | `(validator, key)` for a scalar shorthand | `cv.maybe_simple_value`, `maybe_conf` in `esphome/automation.py` |
| `"templatable"` | the inner validator | `cv.templatable` |
| `"automation"` | the automation schema | `automation.validate_automation` |
Points to watch:
- The dumper matches the function object by `repr`, so the decorated function must be the exact object
placed in the schema. In a factory that returns a new closure per call, decorate the inner function (as
`restore_state_schema` and `cv.maybe_simple_value` do), not the factory.
- The decorator only records anything when extraction is enabled, and that flag is set before imports, so
the decorator must run at import time (module level or inside a factory called at module level).
- A schema chosen per config value (for example by `model`) has no single answer: return a representative
schema, as `model_schema_extractor` does.
- Plain value checks (`cv.All(cv.string, my_check)`) need nothing: the dumper still sees the surrounding
types and keys.
- `cv.Schema`, `cv.typed_schema`, `cv.ensure_list` and the `cv` helpers are already handled; prefer
composing them over a custom function, which also avoids the extractor work.
### Checking the output
Build the language schema into a scratch directory and look at the component's JSON (it takes about ten
seconds):
```bash
python3 script/build_language_schema.py --output-path /tmp/schema
python3 -c "import json; print(json.dumps(json.load(open('/tmp/schema/my_component.json')), indent=1))" | less
```
Every key a user can write should appear under `config_vars` with a sensible `type`; compare against the
same output from `dev` when changing an existing schema. If the script prints `Unknown extracted schema
type` or raises, the decorator type or the value returned for `SCHEMA_EXTRACT` is wrong. Unit tests for
the dumper live in `tests/script/test_build_language_schema.py`.
-169
View File
@@ -1,169 +0,0 @@
---
name: embedded-memory
description: Choose ESPHome C++ containers and storage that avoid heap use and fragmentation on microcontrollers - std::array, StaticVector with cg.slot_counter, FixedVector, std::unique_ptr byte buffers, StringRef, RAMAllocator, and what to avoid (std::vector growth, std::map, std::deque, std::string, new (std::nothrow)). Use when adding or changing C++ members, containers, buffers, strings or registration lists, or when reducing RAM or flash use.
---
# Memory on embedded targets
ESP devices run for months with small heaps shared between Wi-Fi, BLE, LWIP, and application code. Over
time, repeated allocations of different sizes fragment the heap. Failures happen when the largest
contiguous block shrinks, even if total free heap is still large. We have seen field crashes caused by
this.
**Heap allocation after `setup()` should be avoided unless absolutely unavoidable.** Every
allocation/deallocation cycle contributes to fragmentation. ESPHome treats runtime heap allocation as a
long-term reliability bug, not a performance issue. Helpers that hide allocation (`std::string`,
`std::to_string`, string-returning helpers) are being deprecated and replaced with buffer and view based
APIs.
## Container guidelines
1. **Compile-time-known sizes:** use `std::array` instead of `std::vector` when size is known at compile
time.
```cpp
// Bad - generates STL realloc code
std::vector<int> values;
// Good - no dynamic allocation
std::array<int, MAX_VALUES> values;
```
Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration.
**For byte buffers:** avoid `std::vector<uint8_t>` unless the buffer needs to grow. Use
`std::unique_ptr<uint8_t[]>` instead. It does **not** provide bounds checking or iterator support like
`std::vector<uint8_t>`; use it only when you do not need these features.
```cpp
// Bad - STL overhead for simple byte buffer
std::vector<uint8_t> buffer;
buffer.resize(256);
// Good - minimal overhead, single allocation
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(256);
// Or if size is constant:
std::array<uint8_t, 256> buffer;
```
2. **Compile-time-known fixed sizes with vector-like API:** use `StaticVector` from
`esphome/core/helpers.h` for compile-time fixed size with a `push_back()` interface (no dynamic
allocation).
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
std::vector<ServiceRecord> services;
services.reserve(5); // Still includes reallocation machinery
// Good - compile-time fixed size, no dynamic allocation
StaticVector<ServiceRecord, MAX_SERVICES> services;
services.push_back(record1);
```
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
**Listener and child-entity registration lists are the most common case, and the most commonly
missed.** A `register_*()` method called once per child at code generation time has a count that is
known at compile time, so it should never be a `std::vector`. Use `cg.slot_counter()`: it returns a
function that each consumer calls once per slot it will occupy, and after every `to_code` has run it
emits the define with the final count. When nothing registers, no define is emitted and the storage plus
its registration method compile out entirely.
```python
# hub component's __init__.py
_request_listener_slot = cg.slot_counter("MY_COMPONENT_LISTENER_COUNT")
async def register_listener(hub: MockObj, var: MockObj) -> None:
_request_listener_slot()
cg.add(hub.register_listener(var))
```
When several instances each own a list declared at the same size (one per hub of a `MULTI_CONF`
component), pass the owning object as the key, `_request_listener_slot(str(hub))`; the define is then
the largest count any one key requested instead of the total.
```cpp
#ifdef MY_COMPONENT_LISTENER_COUNT
void register_listener(MyComponentListener *listener);
#endif
protected:
#ifdef MY_COMPONENT_LISTENER_COUNT
StaticVector<MyComponentListener *, MY_COMPONENT_LISTENER_COUNT> listeners_;
#endif
```
Request slots from `to_code`, not from a job that runs after `CoroPriority.FINAL`; a late request
raises rather than silently undercounting.
3. **Runtime-known sizes:** use `FixedVector` from `esphome/core/helpers.h` when the size is only known at
runtime initialization.
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
std::vector<TxtRecord> txt_records;
txt_records.reserve(5); // Still includes reallocation machinery
// Good - runtime size, single allocation, no reallocation machinery
FixedVector<TxtRecord> txt_records;
txt_records.init(record_count); // Initialize with exact size at runtime
```
This eliminates `_M_realloc_insert` and `_M_default_append` template instantiations (200-500 bytes per
instance), needs no upper bound, and is compatible with protobuf code generation when using
`[(fixed_vector) = true]`.
4. **Small datasets (1-16 elements):** use `std::vector` or `std::array` with simple structs instead of
`std::map`/`std::set`/`std::unordered_map`.
```cpp
// Bad - 2KB+ overhead for red-black tree/hash table
std::map<std::string, int> small_lookup;
std::unordered_map<int, std::string> tiny_map;
// Good - simple struct with linear search (std::vector is fine)
struct LookupEntry {
const char *key;
int value;
};
std::vector<LookupEntry> small_lookup = {
{"key1", 10},
{"key2", 20},
{"key3", 30},
};
// Or std::array if size is compile-time constant:
// std::array<LookupEntry, 3> small_lookup = {{ ... }};
```
Linear search on small datasets is often faster than hashing or tree overhead, but this depends on
lookup frequency; in hot paths the O(1) vs O(n) difference may still matter. It is the heavy containers
(`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows
otherwise.
5. **Strings set once from configuration:** use `StringRef` (`esphome/core/string_ref.h`) rather than
`std::string`. Code generation passes a string literal that lives in flash for the life of the program,
so storing a `std::string` copies it onto the heap for nothing. `StringRef` is a non-owning pointer plus
length; it does not copy, and it must only ever refer to storage that outlives it (a string literal, or
a buffer owned elsewhere).
```cpp
// Bad - heap copy of a literal that is already in flash
void set_keys(std::string keys) { this->keys_ = std::move(keys); }
std::string keys_;
```
```cpp
// Good - no allocation
void set_keys(const char *keys) { this->keys_ = StringRef(keys); }
StringRef keys_;
```
6. **Avoid `std::deque`:** it allocates in 512-byte blocks regardless of element size, guaranteeing at
least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained
devices.
7. **Never use `new (std::nothrow)`:** on ESP-IDF exceptions are disabled, so a failed nothrow allocation
aborts instead of returning `nullptr`. Use `RAMAllocator` from `esphome/core/helpers.h`; CI rejects
`std::nothrow`.
8. **Detection:** look for these patterns in compiler output:
- Large code sections with STL symbols (vector, map, set)
- `alloc`, `realloc`, `dealloc` in symbol names
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
- Red-black tree code (`rb_tree`, `_Rb_tree`)
- Hash table infrastructure (`unordered_map`, `hash`)
Callback storage (`CallbackManager` vs `LazyCallbackManager`) and templated registration methods are
covered in the `automations` skill.
## Where to spend effort
Prioritize container optimization for core components (API, network, logger), widely used components
(mdns, wifi, ble), and components causing flash size complaints. Avoiding heap allocation after `setup()`
is always required regardless of component type; the priority list is about effort spent on container
migration (e.g. `std::vector` to `StaticVector`).
+2 -2
View File
@@ -17,7 +17,7 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
@@ -32,7 +32,7 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+2 -2
View File
@@ -23,13 +23,13 @@ jobs:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
+4 -4
View File
@@ -62,7 +62,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Set up Docker Buildx
@@ -97,7 +97,7 @@ jobs:
- name: Log in to the GitHub container registry
if: steps.tag.outputs.push == 'true'
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -154,14 +154,14 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to the GitHub container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+7 -7
View File
@@ -34,7 +34,7 @@ jobs:
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
@@ -49,7 +49,7 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -422,7 +422,7 @@ jobs:
version: 1.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Restore integration library cache
@@ -441,7 +441,7 @@ jobs:
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -630,7 +630,7 @@ jobs:
apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@78102a468f96d8e0ccb92d0ec49425753fc42062 # v5.6.0
uses: CodSpeedHQ/action@821a40cd6c80e3cfe387caf55668287addd808da # v5.5.0
with:
run: |
. venv/bin/activate
@@ -1457,7 +1457,7 @@ jobs:
ref: main
path: device-builder
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Set up uv
@@ -1466,7 +1466,7 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
category: "/language:${{matrix.language}}"
+6 -6
View File
@@ -86,7 +86,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.x"
- name: Build
@@ -118,7 +118,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
@@ -126,12 +126,12 @@ jobs:
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to docker hub
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -206,13 +206,13 @@ jobs:
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -71,7 +71,7 @@ jobs:
token: ${{ steps.generate-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
+2 -2
View File
@@ -37,7 +37,7 @@ jobs:
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
@@ -31,7 +31,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
+1 -1
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.17.2
rev: v0.17.1
hooks:
# Run the linter.
- id: ruff
+734 -48
View File
@@ -54,19 +54,6 @@ file does, and it is the authority when they disagree. The most useful starting
`set_interval`, `set_timeout` and `defer`; waking the loop from another thread; the RAM cost of each.
* https://developers.esphome.io/contributing/code/ - contribution rules, public API and breaking changes.
* **Skills:** detailed recipes live in skills under `.agents/skills/` (also reachable as `.claude/skills/` and
`.github/skills/`). Load the matching skill before doing that kind of work; agents without skill support
should read the `SKILL.md` file directly.
* `component-codegen`: component files and metadata, `to_code`, hub child entities, codegen helpers,
`CORE.data` state.
* `config-validation`: schemas, validators, `CONF_` constants, and keeping schema extraction working.
* `automations`: triggers, actions, conditions and callback registration.
* `embedded-memory`: container and string choices that avoid heap use after `setup()`.
* `component-tests`: YAML build tests, bus packages and CI grouping, C++ unit tests, integration tests.
* `breaking-changes`: what is public API, deprecation patterns and migration notes.
* `pr-workflow`, `code-review`, `hardware-test`, `display-test`: creating PRs, reviewing them, and testing
on real boards and panels.
* **Formatting:**
* **Python:** Uses `ruff` for linting and formatting. Configuration is in `pyproject.toml`.
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
@@ -239,6 +226,9 @@ file does, and it is the authority when they disagree. The most useful starting
end of `setup()` when the entities need a state at boot.
* **A multi-step command sequence** leaves the step pending when the answer is malformed, so the existing
resend and give-up logic handles it; never advance on an answer that was only logged.
* **Entities that each watch a register field:** one class taking register, shift, mask and match from a Python
tuple table, in typed `StaticVector`s sized by `cg.slot_counter`; not a template per bit, not a virtual
listener base (a second vptr per entity).
* **A global feature define** says the code is compiled in, not that this instance uses it: keep the
per-instance check (`if (this->aux_device_ == ...)`) inside the `#ifdef`.
* **Model-specific code** sits behind a define the Python side emits only for configs that use that model
@@ -289,28 +279,398 @@ file does, and it is the authority when they disagree. The most useful starting
};
```
* **Component Code Generation:** every component's `__init__.py` defines `DOMAIN` (its own name, checked by
CI). Hub entities go in platform files (`sensor.py`, ...) that take the hub's id, not under the hub's own
key. File layout, metadata, the `to_code` pattern, `sub_sensors` and the other codegen helpers are in the
`component-codegen` skill.
* **Component Structure:**
* **Standard Files:**
```
components/[component_name]/
├── __init__.py # Component configuration schema and code generation
├── [component].h # C++ header file (if needed)
├── [component].cpp # C++ implementation (if needed)
└── [platform]/ # Platform-specific implementations
├── __init__.py # Platform-specific configuration
├── [platform].h # Platform C++ header
└── [platform].cpp # Platform C++ implementation
```
* **Automations (Triggers, Actions, Conditions):** prefer `build_callback_automation()` over a `Trigger`
class, and `register_apply_action` / `register_apply_condition` over a C++ class that only forwards to its
parent. Callback registration methods are always `template<typename F>`, never `std::function`. Details
and examples are in the `automations` skill.
* **Component Metadata:**
- `DOMAIN`: Required in every component's `__init__.py`, set to the component's own name as a plain
string (e.g. `DOMAIN = "my_component"`). Other code refers to the component through it, e.g.
`from esphome.components.my_component import DOMAIN`. Keep it in alphabetical order with the other
metadata constants. CI (`lint_component_domain`) fails without it.
- `DEPENDENCIES`: List of required components
- `AUTO_LOAD`: Components to automatically load. Not a way to put entities under the component's
own key; see "Entities of a hub" below.
- `CONFLICTS_WITH`: Incompatible components
- `CODEOWNERS`: GitHub usernames responsible for maintenance
- `MULTI_CONF`: Whether multiple instances are allowed
* **Code Generation & Common Patterns:**
* **Configuration Schema Pattern:**
```python
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_KEY, CONF_ID
DOMAIN = "my_component"
CONF_PARAM = "param" # A constant that does not yet exist in esphome/const.py
my_component_ns = cg.esphome_ns.namespace("my_component")
MyComponent = my_component_ns.class_("MyComponent", cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(MyComponent),
cv.Required(CONF_KEY): cv.string,
cv.Optional(CONF_PARAM, default=42): cv.int_,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_key(config[CONF_KEY]))
cg.add(var.set_param(config[CONF_PARAM]))
```
* **C++ Class Pattern:**
```cpp
namespace esphome::my_component {
class MyComponent : public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
void set_key(const std::string &key) { this->key_ = key; }
void set_param(int param) { this->param_ = param; }
protected:
std::string key_;
int param_{0};
};
} // namespace esphome::my_component
```
* **Common Component Examples:**
- **Sensor:**
```python
from esphome.components import sensor
CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(
cv.polling_component_schema("60s")
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
```
- **Binary Sensor:**
```python
from esphome.components import binary_sensor
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({...})
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
```
- **Switch:**
```python
from esphome.components import switch
CONFIG_SCHEMA = switch.switch_schema().extend({...})
async def to_code(config):
var = await switch.new_switch(config)
```
- **Entities of a hub:** a new component's entities go in platform files (`sensor.py`,
`binary_sensor.py`, ...) that take the hub's id, never under the hub's own key with their platforms
added to `AUTO_LOAD` to make that work. Auto loading a platform because the C++ uses its type is
fine. See https://developers.esphome.io/architecture/components/#exposing-multiple-entities.
```yaml
# Bad - entities under the hub's key
my_hub:
connected:
name: Connected
# Good - the hub, then one platform entry that holds its entities
my_hub:
id: hub1
binary_sensor:
- platform: my_hub
my_hub_id: hub1 # optional with a single hub
connected:
name: Connected
```
- **Optional child entities of a hub:** bind the config once with `sensor.sub_sensors(config)` (or
`sub_binary_sensors`, `sub_text_sensors`, `sub_buttons`, `sub_switches`, `sub_numbers`,
`sub_selects` in their domains), adding `parent=hub` for entities that derive from `Parented<T>`,
then make one call per key, even when there is only one. A call creates the entity only when its key
is configured, passes it to the setter and returns it (or `None`); extra arguments such as
`min_value` or `options` go on the call. Always name the setter explicitly on the object that owns
it, never with `getattr` and an f-string, and keep that variable short (`var` for the component
itself, `hub` for one fetched with `cg.get_variable`) so the calls fit on one line. Loops whose
setter also takes an index, such as `set_gate_threshold(x, n)`, stay as they are.
```python
# my_hub/sensor.py
async def to_code(config):
hub = await cg.get_variable(config[CONF_MY_HUB_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, hub.set_temperature_sensor)
await sensors(CONF_HUMIDITY, hub.set_humidity_sensor)
# my_hub/button.py
async def to_code(config):
hub = await cg.get_variable(config[CONF_MY_HUB_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_RESTART, hub.set_restart_button)
```
- **Pins and referenced components:** set them with `await cg.add_pin(var.set_cs_pin, config[CONF_CS_PIN])`
and `await cg.add_variable(var.set_bus, config[CONF_BUS_ID])`; both skip a `None` value, so pass
`config.get(KEY)` for an optional key, and return what they created. Use `cg.register_parented` for `set_parent`.
- **Hub forwarding entities need no C++ class:** a button, switch, number or select that only calls its hub
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`)
constructs, registers and attaches in one call; `display.add_writer(var, config)` sets the lambda;
`MyEnum.options("on", prefix="MY_ENUM_")` builds an enum dict; `button.restart_button_schema(cls)` (and
`factory_reset_`, `query_`) are button presets; `fv.get_declaring_config(id)` finds a hub's config in
final validation.
* **Automations (Triggers, Actions, Conditions):**
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
* **Triggers -- Callback method (preferred):**
Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID` in the schema.
**Python:**
```python
from esphome import automation
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(MyComponent),
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
for conf in config.get(CONF_ON_STATE, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [(bool, "x")], conf
)
```
`build_callback_automation` arguments: `parent`, `callback_method` (C++ method name), `args` (template args as `[(type, name)]` tuples), `config`, and optional `forwarder` (defaults to `TriggerForwarder<Ts...>`).
For boolean filtering (e.g. `on_press`/`on_release`), use built-in forwarders with `args=[]`:
```python
for conf_key, forwarder in (
(CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
(CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=forwarder
)
```
When the callback's parameters differ from the automation's arguments, or the trigger should only
fire for some values, pass `params`, `forward` and `when`; the helper then generates a capture-less
lambda (stored inline, nothing allocated). Name the parent with `automation.parent_ref(var)` and build
`forward` from `MockObj` calls, not f-strings of C++:
```python
parent = automation.parent_ref(var)
index = cg.RawExpression("index")
await automation.build_callback_automation(
var,
"add_on_state_callback",
[(cg.StringRef, "x"), (cg.size_t, "i")],
conf,
params=[(cg.size_t, "index")],
forward=[cg.StringRef(parent.option_at(index)), index],
)
```
`build_parent_callback_automation`, `build_trigger_callback`, `build_callback_automations` and
`build_trigger_automations` cover the other shapes; see their docstrings in `esphome/automation.py`.
**C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation):
```cpp
class MyComponent : public Component {
public:
// Must be a template -- accepts both std::function and pointer-sized forwarder structs
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
protected:
// Use CallbackManager when callbacks are always registered (e.g. core components)
CallbackManager<void(bool)> state_callback_;
// Use LazyCallbackManager when callbacks are often not registered -- saves 8 bytes
// (nullptr vs empty std::vector) per instance when no callbacks are added
// LazyCallbackManager<void(bool)> state_callback_;
};
```
* **Triggers -- Trigger class method:**
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic).
**Python:**
```python
TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template())
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)}
),
}
)
async def to_code(config):
for conf in config.get(CONF_ON_TURN_ON, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
```
**C++:**
```cpp
class TurnOnTrigger : public Trigger<> {
public:
explicit TurnOnTrigger(MyComponent *parent) : last_on_{false} {
parent->add_on_state_callback([this](bool state) {
if (state && !this->last_on_)
this->trigger();
this->last_on_ = state;
});
}
protected:
bool last_on_;
};
```
* **Actions:**
```cpp
template<typename... Ts> class MyAction : public Action<Ts...> {
public:
explicit MyAction(MyComponent *parent) : parent_(parent) {}
void play(const Ts &...) override { this->parent_->do_something(); }
protected:
MyComponent *parent_;
};
```
Register it without writing a builder:
```python
automation.register_simple_action(
"my_component.do_something", MyAction, schema, synchronous=True
)
```
The constructor receives the object named by `config[CONF_ID]`. Use `register_bare_action` for a
no-argument constructor, `register_parented_action` for a class deriving from `Parented<T>`, and
the `@automation.register_action(...)` decorator only when the builder must also set fields.
Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
**Actions that only forward templatable values to their parent need no C++ class.** Register them
with `register_apply_action`; do not write a `TEMPLATABLE_VALUE` class or a builder for this shape.
```python
automation.register_apply_action(
"my_component.set_gains",
schema,
automation.ApplyField(CONF_KP, "set_kp", cg.float_),
automation.ApplyField(CONF_KI, "set_ki", cg.float_),
)
```
The `ApplyField`, `ApplyCall` and `register_apply_action` docstrings in `esphome/automation.py` cover
the rest; `cover.control` and `cover.template.publish` are in-tree examples. An action whose input is a
byte payload passes `automation.ApplyBytes(CONF_DATA, "send({})")` in the same call; a constant payload
stays in flash and a lambda's vector is forwarded as data and size. `TEMPLATABLE_VALUE` with
`cg.templatable` stays for actions whose `play()` has real logic beyond forwarding values.
* **Conditions:**
```cpp
template<typename... Ts> class MyCondition : public Condition<Ts...> {
public:
explicit MyCondition(MyComponent *parent) : parent_(parent) {}
bool check(const Ts &...) override { return this->parent_->is_active(); }
protected:
MyComponent *parent_;
};
```
Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`;
`register_bare_condition`, `register_parented_condition` and the decorator follow the action rules.
**Conditions that only test their parent need no C++ class either.** Register them with
`register_apply_condition`; the expression is applied to the parent, and an `ApplyCall` compares
against config values.
```python
automation.register_apply_condition("my_component.is_active", schema, "is_active()")
automation.register_apply_condition(
"my_component.state_is",
schema,
automation.ApplyCall("state == {}", ((CONF_STATE, cg.bool_),)),
)
```
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
An intentionally empty body (an unused `register_dumper` builder, for example) gets a one-line docstring, not `pass`.
* **Configuration Validation:** follow the `config-validation` skill. In short: one `cv.typed_schema` per
model, reuse validators from `config_validation.py`, and never add `CONF_` constants to the frozen
`esphome/const.py`.
* **Schema extraction:** whenever config validation code is added or changed, make sure
`script/build_language_schema.py` still extracts the full schema. The dumper cannot see inside a plain
function, so a validator function that wraps, chooses or builds a schema must be decorated with
`@schema_extractor(...)` and return the schema data when called with `SCHEMA_EXTRACT` (both from
`esphome.schema_extractors`). CI does not catch a missing schema; build the schema and check the
component's JSON as the `config-validation` skill describes.
* **Configuration Validation:**
* **One schema per model:** when a component's options depend on a `model` key, use `cv.typed_schema({...},
key=CONF_MODEL, default_type=...)` with each model's own keys and defaults, not one schema plus a validator
that rejects keys and fills defaults afterwards.
* **Reuse existing validators:** Before writing a custom validator, check for an existing one in `config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components. For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`.
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
* **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`, `cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`.
* **Framework-Specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`. On the ESP32, Arduino is
built as a component of ESP-IDF, so an ESP-IDF API, sdkconfig option or version floor is there on both
frameworks: gate on `esp32.require_idf_version(cv.Version(5, 5, 3), "psram_dma")`, which checks the
platform and the ESP-IDF under either framework, as `spi`, `usb_host` and `wifi` do. Reserve
`cv.only_with_framework("esp-idf")` for a build that has been tried on Arduino and fails there, and write
the actual failure next to it (`sendspin`: arduino-esp32 below ESP-IDF 6.0 bundles a second libsodium,
and from 6.0 sendspin-cpp's enumerators collide with `Arduino.h` macros such as `DISPLAY`).
* **Schema Extensions:**
```python
CONFIG_SCHEMA = cv.Schema({ ... })
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(i2c.i2c_device_schema(0x48))
.extend(spi.spi_device_schema(cs_pin_required=True))
```
* **Constants:** `esphome/const.py` is frozen — do not add new `CONF_` constants there. Define a component-local constant in the component's own `.py` (as with `CONF_PARAM` above); for a constant shared by multiple components, add it to `esphome/components/const/__init__.py`. CI (`lint_constants_usage`) fails if the same constant is defined in three or more component files. Constants used in core files (i.e. those not under `esphome/components`) may be added to `esphome/const.py` but will require adjustment to the CI validation check.
## 5. Key Files & Entrypoints
@@ -330,12 +690,71 @@ file does, and it is the authority when they disagree. The most useful starting
* **Testing:**
* **Python:** Run unit tests with `pytest`.
* **C++:** Use `clang-tidy` for static analysis.
* **Component tests:** YAML build tests live in `tests/components/<component>/` and run with
`script/test_build_components -c <component>`. Never define buses directly in test YAML: include the
shared packages from `tests/test_build_components/common/` with dict-style `packages:`. Test file naming,
`validate.*.yaml` config-only tests, CI grouping, C++ unit tests and integration tests are in the
`component-tests` skill.
* **C++ unit tests:** gtests in `tests/components/<component>/*.cpp`, run with
`script/cpp_unit_test.py <component>`; see `tests/components/README.md`. Set the loop clock in a test
with `LoopBlockingGuard{nullptr, nullptr, now_ms}`; the runner resets it before every test.
* **Integration tests:** `tests/integration/` runs real firmware on the host. Serial components prove
timeouts and recovery with the `uart_mock` fixture (`tests/integration/test_uart_mock_*.py`). Its
`responses` match on content every time they see it; to answer a repeated request differently, use an
`on_tx` lambda with a static counter and `inject_to_rx_buffer` (see `uart_mock_ld2412_requery_after_giveup.yaml`).
* **Keep build-test variants few:** one `test.<platform>.yaml` per platform the code actually differs
on. A variant per board for platform-identical code only adds CI time.
* **A test that needs a shared component in a particular state** (`esp32_ble` security settings, an option
that PSRAM rejects) cannot be grouped: the merged block wins and the whole batch fails. Add the component to
`ISOLATED_COMPONENTS` in `script/analyze_component_buses.py`, or move the option to a `test-<variant>` file.
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
```
tests/
├── test_build_components/
│ └── common/ # Shared bus packages (uart, i2c, spi, etc.)
│ ├── uart/ # UART at default baud rate
│ ├── uart_115200/ # UART at 115200 baud
│ ├── i2c/ # I2C bus
│ └── spi/ # SPI bus
└── components/[component]/
├── common.yaml # Component-only config (no bus definitions)
├── test.esp32-idf.yaml # config + compile
├── test.esp8266-ard.yaml # config + compile
├── test-variant.esp32-idf.yaml # variant test, config + compile
├── validate.esp32-idf.yaml # config-only (never compiled)
└── validate-legacy.esp32-idf.yaml # config-only variant
```
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
* **Config-only test files (`validate.*.yaml`):** Use this prefix when a YAML file only needs to exercise schema/validation paths and does not need to be compiled. CI runs `validate.*.yaml` files with `esphome config` only and skips them during compile. The grammar mirrors `test.*.yaml`:
- `validate.<platform>.yaml` — base config-only test
- `validate-<variant>.<platform>.yaml` — config-only variant
Use this for things like deprecated-syntax migration tests, schema edge cases, or platform-specific validation branches where building firmware adds no signal. A component may have any mix of `test.*.yaml` and `validate.*.yaml` files. Validate files never participate in bus-grouping; each one runs as its own `esphome config` invocation.
When a PR's only edits to a component are `validate.*.yaml` files (no source changes, no `test.*.yaml` changes, and the component isn't pulled in as a dependency of another changed component), CI skips the compile stage for that component entirely and only runs config validation. This is decided in `script/determine-jobs.py` via `_component_change_is_validate_only` and surfaced as the `validate_only_components` output that the `test-build-components-split` job consumes.
* **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`.
All includes in test files must go through dict-style `packages:` so that batch grouping works correctly — the grouping scripts only understand dict-style packages. Never use list-style packages (`packages: [- !include ...]`) or top-level merge keys (`<<: !include common.yaml`). Bus packages are keyed by the bus name; the component's `common.yaml` is keyed by the component name (e.g. `cst328: !include common.yaml`):
```yaml
# test.esp32-idf.yaml — everything included via named packages
packages:
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
my_component: !include common.yaml
```
```yaml
# common.yaml — component config only, NO bus definitions
my_component:
id: my_instance
sensor:
- platform: my_component
name: My Sensor
```
Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time.
`!extend` and `!remove` in a `test.*.yaml` also make the component ungroupable; add a standalone entity with its own id instead.
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
```bash
./script/test_component_grouping.py -e config --all
```
This tests all components in a single build to catch conflicts that might not appear when testing components individually. Use `-e config` for fast configuration validation, or `-e compile` for full compilation testing.
* **Debugging and Troubleshooting:**
* **Debug Tools:**
- `esphome config <file>.yaml` to validate configuration.
@@ -349,7 +768,7 @@ file does, and it is the authority when they disagree. The most useful starting
## 7. Specific Instructions for AI Collaboration
* **Contribution Workflow (Pull Request Process):** the `pr-workflow` skill walks through these steps.
* **Contribution Workflow (Pull Request Process):**
1. **Fork & Branch:** Create a new branch based on the `dev` branch (always use `git checkout -b <branch-name> dev` to ensure you're branching from `dev`, not the currently checked out branch).
2. **Make Changes:** Adhere to all coding conventions and patterns.
3. **Test:** Create component tests for all supported platforms and run the full test suite locally.
@@ -369,15 +788,236 @@ file does, and it is the authority when they disagree. The most useful starting
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
* **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage.
**Heap allocation after `setup()` should be avoided unless absolutely unavoidable.** ESP devices run for
months with small shared heaps, and repeated allocations fragment them until a large allocation fails; we
have seen field crashes caused by this. ESPHome treats runtime heap allocation as a long-term reliability
bug, not a performance issue. Prefer `std::array`, `StaticVector` (sized with `cg.slot_counter()` for
registration lists), `FixedVector` and `StringRef` over growing `std::vector`, `std::map`, `std::deque`
and `std::string`. The `embedded-memory` skill has the full container guidelines.
**Why Heap Allocation Matters:**
* **State Management:** keep code generation state in `CORE.data`, namespaced under the component's
`DOMAIN` in a `@dataclass`, never in module-level mutable globals (see the `component-codegen` skill).
ESP devices run for months with small heaps shared between Wi-Fi, BLE, LWIP, and application code. Over time, repeated allocations of different sizes fragment the heap. Failures happen when the largest contiguous block shrinks, even if total free heap is still large. We have seen field crashes caused by this.
**Heap allocation after `setup()` should be avoided unless absolutely unavoidable.** Every allocation/deallocation cycle contributes to fragmentation. ESPHome treats runtime heap allocation as a long-term reliability bug, not a performance issue. Helpers that hide allocation (`std::string`, `std::to_string`, string-returning helpers) are being deprecated and replaced with buffer and view based APIs.
**STL Container Guidelines:**
ESPHome runs on embedded systems with limited resources. Choose containers carefully:
1. **Compile-time-known sizes:** Use `std::array` instead of `std::vector` when size is known at compile time.
```cpp
// Bad - generates STL realloc code
std::vector<int> values;
// Good - no dynamic allocation
std::array<int, MAX_VALUES> values;
```
Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration.
**For byte buffers:** Avoid `std::vector<uint8_t>` unless the buffer needs to grow. Use `std::unique_ptr<uint8_t[]>` instead.
> **Note:** `std::unique_ptr<uint8_t[]>` does **not** provide bounds checking or iterator support like `std::vector<uint8_t>`. Use it only when you do not need these features and want minimal overhead.
```cpp
// Bad - STL overhead for simple byte buffer
std::vector<uint8_t> buffer;
buffer.resize(256);
// Good - minimal overhead, single allocation
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(256);
// Or if size is constant:
std::array<uint8_t, 256> buffer;
```
2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for compile-time fixed size with `push_back()` interface (no dynamic allocation).
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
std::vector<ServiceRecord> services;
services.reserve(5); // Still includes reallocation machinery
// Good - compile-time fixed size, no dynamic allocation
StaticVector<ServiceRecord, MAX_SERVICES> services;
services.push_back(record1);
```
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
**Listener and child-entity registration lists are the most common case, and the most commonly
missed.** A `register_*()` method called once per child at code generation time has a count that
is known at compile time, so it should never be a `std::vector`. Use `cg.slot_counter()`: it
returns a function that each consumer calls once per slot it will occupy, and after every
`to_code` has run it emits the define with the final count. When nothing registers, no define is
emitted and the storage plus its registration method compile out entirely.
```python
# hub component's __init__.py
_request_listener_slot = cg.slot_counter("MY_COMPONENT_LISTENER_COUNT")
async def register_listener(hub: MockObj, var: MockObj) -> None:
_request_listener_slot()
cg.add(hub.register_listener(var))
```
When several instances each own a list declared at the same size (one per hub of a
`MULTI_CONF` component), pass the owning object as the key, `_request_listener_slot(str(hub))`;
the define is then the largest count any one key requested instead of the total.
```cpp
#ifdef MY_COMPONENT_LISTENER_COUNT
void register_listener(MyComponentListener *listener);
#endif
protected:
#ifdef MY_COMPONENT_LISTENER_COUNT
StaticVector<MyComponentListener *, MY_COMPONENT_LISTENER_COUNT> listeners_;
#endif
```
Request slots from `to_code`, not from a job that runs after `CoroPriority.FINAL` - a late
request raises rather than silently undercounting.
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
std::vector<TxtRecord> txt_records;
txt_records.reserve(5); // Still includes reallocation machinery
// Good - runtime size, single allocation, no reallocation machinery
FixedVector<TxtRecord> txt_records;
txt_records.init(record_count); // Initialize with exact size at runtime
```
**Benefits:**
- Eliminates `_M_realloc_insert`, `_M_default_append` template instantiations (saves 200-500 bytes per instance)
- Single allocation, no upper bound needed
- No reallocation overhead
- Compatible with protobuf code generation when using `[(fixed_vector) = true]` option
4. **Small datasets (1-16 elements):** Use `std::vector` or `std::array` with simple structs instead of `std::map`/`std::set`/`std::unordered_map`.
```cpp
// Bad - 2KB+ overhead for red-black tree/hash table
std::map<std::string, int> small_lookup;
std::unordered_map<int, std::string> tiny_map;
// Good - simple struct with linear search (std::vector is fine)
struct LookupEntry {
const char *key;
int value;
};
std::vector<LookupEntry> small_lookup = {
{"key1", 10},
{"key2", 20},
{"key3", 30},
};
// Or std::array if size is compile-time constant:
// std::array<LookupEntry, 3> small_lookup = {{ ... }};
```
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
5. **Strings set once from configuration:** Use `StringRef` (`esphome/core/string_ref.h`) rather than
`std::string`. Code generation passes a string literal that lives in flash for the life of the
program, so storing a `std::string` copies it onto the heap for nothing. `StringRef` is a
non-owning pointer plus length; it does not copy, and it must only ever refer to storage that
outlives it (a string literal, or a buffer owned elsewhere).
```cpp
// Bad - heap copy of a literal that is already in flash
void set_keys(std::string keys) { this->keys_ = std::move(keys); }
std::string keys_;
```
```cpp
// Good - no allocation
void set_keys(const char *keys) { this->keys_ = StringRef(keys); }
StringRef keys_;
```
6. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
7. **Never use `new (std::nothrow)`:** On ESP-IDF exceptions are disabled, so a failed nothrow allocation aborts instead of returning `nullptr`. Use `RAMAllocator` from `esphome/core/helpers.h`; CI rejects `std::nothrow`.
8. **Detection:** Look for these patterns in compiler output:
- Large code sections with STL symbols (vector, map, set)
- `alloc`, `realloc`, `dealloc` in symbol names
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
- Red-black tree code (`rb_tree`, `_Rb_tree`)
- Hash table infrastructure (`unordered_map`, `hash`)
**Prioritize optimization effort for:**
- Core components (API, network, logger)
- Widely-used components (mdns, wifi, ble)
- Components causing flash size complaints
Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`).
**Callback Managers:**
ESPHome provides two callback manager types in `esphome/core/helpers.h` for the observer pattern. Both support `std::function`, lambdas, and lightweight forwarder structs via their templatized `add()` method.
| Type | Idle overhead (32-bit) | When to use |
|------|----------------------|-------------|
| `CallbackManager<void(Ts...)>` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered |
| `LazyCallbackManager<void(Ts...)>` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) |
`LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first callback is added. Prefer it for entity-level callbacks where most instances have no subscribers.
**Important:** Registration methods that add to a callback manager **must always be templatized** to accept both `std::function` and pointer-sized forwarder structs (used by `build_callback_automation`). Never use `std::function` in the method signature:
```cpp
// Bad -- forces heap allocation for forwarder structs
void add_on_state_callback(std::function<void(bool)> &&callback) {
this->state_callback_.add(std::move(callback));
}
// Good -- accepts any callable without forcing std::function wrapping
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
```
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
**Bad Pattern (Module-Level Globals):**
```python
# Don't do this - state persists between compilation runs
_component_state = []
_use_feature = None
def enable_feature():
global _use_feature
_use_feature = True
```
**Bad Pattern (Flat Keys):**
```python
# Don't do this - keys should be namespaced under component domain
MY_FEATURE_KEY = "my_component_feature"
CORE.data[MY_FEATURE_KEY] = True
```
**Good Pattern (dataclass):**
```python
from dataclasses import dataclass, field
from esphome.core import CORE
DOMAIN = "my_component"
@dataclass
class MyComponentData:
feature_enabled: bool = False
item_count: int = 0
items: list[str] = field(default_factory=list)
def _get_data() -> MyComponentData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = MyComponentData()
return CORE.data[DOMAIN]
def request_feature() -> None:
_get_data().feature_enabled = True
def add_item(item: str) -> None:
_get_data().items.append(item)
```
If you need a real-world example, search for components that use `@dataclass` with `CORE.data` in the codebase. Note: Some components may use `TypedDict` for dictionary-based storage; both patterns are acceptable depending on your needs.
**Why this matters:**
- Module-level globals persist between compilation runs if the host process (e.g. device-builder) doesn't fork/exec
- `CORE.data` automatically clears between runs
- Namespacing under `DOMAIN` prevents key collisions between components
- `@dataclass` provides type safety and cleaner attribute access
* **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys.
@@ -388,11 +1028,57 @@ file does, and it is the authority when they disagree. The most useful starting
## 8. Public API and Breaking Changes
* Only features documented at [esphome.io](https://esphome.io) are public API for components; all `public`
members of `esphome/core/` and base classes are public API. Aim for a 6-month deprecation window
(`ESPDEPRECATED` in C++, `cv.rename_key(..., removed_in=...)` for config keys) and document the migration
path in the PR description. The `breaking-changes` skill has the full policy, checklist and patterns.
* **Public C++ API:**
* **Components**: Only documented features at [esphome.io](https://esphome.io) are public API. Undocumented `public` members are internal.
* **Core/Base Classes** (`esphome/core/`, `Component`, `Sensor`, etc.): All `public` members are public API.
* **Components with Global Accessors** (`global_api_server`, etc.): All `public` members are public API (except config setters).
* **Public Python API:**
* All documented configuration options at [esphome.io](https://esphome.io) are public API.
* Python code in `esphome/core/` actively used by existing core components is considered stable API.
* Other Python code is internal unless explicitly documented for external component use.
* **Breaking Changes Policy:**
* Aim for **6-month deprecation window** when possible
* Clean breaks allowed for: signature changes, deep refactorings, resource constraints
* Must document migration path in PR description (generates release notes)
* Blog post required for core/base class changes or significant architectural changes
* Full details: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes
* **Breaking Change Checklist:**
- [ ] Clear justification (RAM/flash savings, architectural improvement)
- [ ] Explored non-breaking alternatives
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
- [ ] Documented migration path in PR description with before/after examples
- [ ] Updated all internal usage and esphome.io
- [ ] Tested backward compatibility during deprecation period
* **Deprecation Pattern (C++):**
```cpp
// Remove before 2026.6.0
ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0")
void old_method() { this->new_method(); }
```
* **Deprecation Pattern (Python):**
For a renamed config key, use the shared `cv.rename_key` validator with `removed_in` (and `component` for context) — it warns and auto-migrates:
```python
CONFIG_SCHEMA = cv.All(
cv.rename_key(
CONF_OLD_KEY, CONF_NEW_KEY, removed_in="2026.6.0", component="my_component"
),
cv.Schema({...}),
)
```
For other deprecations, warn manually during validation:
```python
# Remove before 2026.6.0
if CONF_OLD_KEY in config:
_LOGGER.warning(
f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0"
)
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
```
## 9. English Language
The project uses English for non-code content. When drafting documentation, code comments, commit messages,
+1 -1
View File
@@ -439,7 +439,6 @@ esphome/components/modbus_controller/select/* @martgras @stegm
esphome/components/modbus_controller/sensor/* @martgras
esphome/components/modbus_controller/switch/* @martgras
esphome/components/modbus_controller/text_sensor/* @martgras
esphome/components/modbus_monitor/* @Bascht74
esphome/components/modbus_server/* @exciton
esphome/components/modem/* @oarcher
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
@@ -635,6 +634,7 @@ esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004
esphome/components/stcc4/* @j9brown
esphome/components/store_yaml/* @bdraco
esphome/components/stts22h/* @B48D81EFCC
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
+16 -21
View File
@@ -17,7 +17,7 @@ from typing import TYPE_CHECKING, Protocol
# cause them to be loaded before external components are processed, resulting
# in the built-in version being used instead of the external component one.
from esphome import const, platform_hooks
from esphome.build_helpers.native import native_backend
from esphome.build_helpers.native import analysis_backend, native_backend
from esphome.const import (
ARGUMENT_HELP_DEVICE,
BUNDLE_EXTENSION,
@@ -1305,7 +1305,10 @@ def upload_program(
f"{option_string} is only supported with the esphome OTA platform; "
"the web_server OTA path can only update the firmware image."
)
return _upload_via_web_server(config, network_devices, _app_ota_image(args))
binary = CORE.firmware_bin
if getattr(args, "file", None) is not None:
binary = Path(args.file)
return _upload_via_web_server(config, network_devices, binary)
return _upload_via_native_api(config, network_devices, args)
@@ -1361,16 +1364,6 @@ def _esphome_ota_conf(config: ConfigType) -> ConfigType | None:
return next(iter(esphome_ota_items(config)), None)
def _app_ota_image(args: ArgsProtocol) -> Path:
"""The file an app update sends: ``--file`` or the build output, as the
target platform's ota_image hook picks it."""
binary = CORE.firmware_bin
if (file := getattr(args, "file", None)) is not None:
binary = Path(file)
ota_image = platform_hooks.get_platform_hook(CORE.target_platform, "ota_image")
return ota_image(binary) if ota_image is not None else binary
def _upload_via_native_api(
config: ConfigType, network_devices: list[str], args: ArgsProtocol
) -> tuple[int, str | None]:
@@ -1420,27 +1413,27 @@ def _upload_via_native_api(
f"flash a build with the option enabled, and then retry {option_string}."
)
binary = CORE.firmware_bin
ota_type = espota2.OTA_TYPE_UPDATE_APP
file = getattr(args, "file", None)
if getattr(args, "partition_table", False):
# Fail fast if the resolved ESPHome OTA config does not enable allow_partition_access.
# The device-side handshake also rejects this with "Device only supports app updates",
# but checking here surfaces the misconfiguration before opening a network connection.
check_partition_access("--partition-table")
binary = Path(file) if file is not None else CORE.partition_table_bin
binary = CORE.partition_table_bin
ota_type = espota2.OTA_TYPE_UPDATE_PARTITION_TABLE
elif getattr(args, "bootloader", False):
check_partition_access("--bootloader")
if (
file is None
getattr(args, "file", None) is None
and (native := native_backend())
and (hint := getattr(native, "missing_image_hint", lambda: None)())
):
raise EsphomeError(hint)
binary = Path(file) if file is not None else CORE.bootloader_bin
binary = CORE.bootloader_bin
ota_type = espota2.OTA_TYPE_UPDATE_BOOTLOADER
else:
binary = _app_ota_image(args)
if getattr(args, "file", None) is not None:
binary = Path(args.file)
if ota_type == espota2.OTA_TYPE_UPDATE_PARTITION_TABLE:
_validate_partition_table_binary(binary)
@@ -2138,7 +2131,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
# Refuse an unsupported toolchain before paying for a full compile
analysis_toolchain = native_backend()
analysis_toolchain = analysis_backend()
if analysis_toolchain is None and not CORE.using_toolchain_platformio:
_LOGGER.error(
"analyze-memory is not supported with the '%s' toolchain on %s; "
@@ -2147,9 +2140,11 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
CORE.target_platform,
)
return 1
if hasattr(analysis_toolchain, "check_analysis_supported"):
if (
check_supported := getattr(analysis_toolchain, "check_analysis_supported", None)
) is not None:
# Raises with the reason; before the compile, not after it
analysis_toolchain.check_analysis_supported()
check_supported()
# Always compile to ensure fresh data (fast if no changes - just relinks)
exit_code = write_cpp(config)
+6 -54
View File
@@ -25,7 +25,6 @@ from esphome.framework_helpers import (
get_project_link_flags,
)
from esphome.helpers import get_bool_env, mkdir_p, write_file_if_changed
from esphome.platformio.library import lex_build_flags
_LOGGER = logging.getLogger(__name__)
@@ -155,47 +154,6 @@ def has_discovered_components() -> bool:
return bool(get_available_components())
_KEY_WARNED_BUILD_FLAGS = "warned_build_flags"
def _lexed_build_flags() -> list[str]:
"""Shell-lex the build flag entries as the other generators do, so an
entry holding several flags is classified flag by flag. Warns once per
entry about a flag that native ESP-IDF builds do not pass on."""
warned: set[tuple[str, str]] = CORE.data.setdefault(KEY_ESP32, {}).setdefault(
_KEY_WARNED_BUILD_FLAGS, set()
)
flags: list[str] = []
for entry in sorted(CORE.build_flags):
tokens = lex_build_flags(entry, "esphome build_flags")
for token in tokens:
if (
(token, entry) not in warned
and not get_project_compile_flags([token])
and not get_project_link_flags([token])
):
warned.add((token, entry))
_LOGGER.warning(
"Build flag '%s'%s is ignored: native ESP-IDF builds only "
"pass -D, -W and -Wl, flags",
token,
"" if token == entry else f" in '{entry}'",
)
flags += tokens
return flags
def _cmake_args(flags: list[str]) -> list[str]:
"""Quote each flag as a CMake argument so quotes, backslashes and $
survive."""
return [
'"{}"'.format(
flag.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$")
)
for flag in flags
]
def _cmake_quote(value: str) -> str:
"""Quote a cmake arg value for a set() line. add_cmake_arg rejects
whitespace, quotes, and '$', so only backslashes need escaping."""
@@ -336,24 +294,18 @@ def get_project_cmakelists(
# esphome__micro-mp3) rather than just src/. Required so suppressions
# like ``-Wno-error=maybe-uninitialized`` actually silence warnings in
# third-party components we don't author.
# CMake keeps only the first copy of a repeated option; drop all but the
# last copy instead so the last definition wins, as on the command line.
project_compile_opts = list(
reversed(
dict.fromkeys(reversed(get_project_compile_flags(_lexed_build_flags())))
)
)
project_compile_opts = get_project_compile_flags()
extra_compile_options = "\n".join(
f"idf_build_set_property(COMPILE_OPTIONS {flag} APPEND)"
for flag in _cmake_args(project_compile_opts)
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
for flag in project_compile_opts
)
# Flags registered via cg.add_cxx_build_flag() go on CXX_COMPILE_OPTIONS
# (not COMPILE_OPTIONS) because GCC warns when a C++-only flag such as
# -Wno-volatile is passed on a C compile.
cxx_compile_options = "\n".join(
f"idf_build_set_property(CXX_COMPILE_OPTIONS {flag} APPEND)"
for flag in _cmake_args(get_project_cxx_compile_flags())
f'idf_build_set_property(CXX_COMPILE_OPTIONS "{flag}" APPEND)'
for flag in get_project_cxx_compile_flags()
)
cpp_standard_options = (
@@ -509,7 +461,7 @@ def get_component_cmakelists() -> str:
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
# emitted project-wide via idf_build_set_property in
# get_project_cmakelists so they reach every component, not just src/.
link_opts = _cmake_args(get_project_link_flags(_lexed_build_flags()))
link_opts = get_project_link_flags()
link_opts_str = "\n ".join(link_opts) if link_opts else ""
src_cmake = "\n".join(CORE.data[KEY_ESP32].get(KEY_SRC_CMAKE, []))
+5 -5
View File
@@ -93,7 +93,7 @@ def _split_command(command: str) -> list[str]:
CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)]
CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR)
argc = ctypes.c_int(0)
argc = ctypes.c_int()
argv = CommandLineToArgvW(command, ctypes.byref(argc))
if not argv: # pragma: no cover
raise ctypes.WinError()
@@ -296,10 +296,10 @@ def load_or_build_idedata(
) -> dict | None:
"""Return idedata for a compile_commands.json build, cached on mtime.
Shared by the native toolchains. Returns None when the compile DB
doesn't exist yet (nothing was built). ``launcher`` is the
compiler-launcher path (ccache) the build was generated with, if any;
commands in the compile DB are prefixed with it.
Shared by the native ESP-IDF and ESP8266 Arduino toolchains. Returns None
when the compile DB doesn't exist yet (nothing was built). ``launcher``
is the compiler-launcher path (ccache) the build was generated with, if
any; commands in the compile DB are prefixed with it.
"""
if not compile_commands.is_file():
_LOGGER.debug("No %s yet; skipping idedata generation", compile_commands)
+18 -1
View File
@@ -22,7 +22,6 @@ NATIVE_TOOLCHAIN_MODULES = {
("esp32", Toolchain.ESP_IDF): "esphome.espidf.toolchain",
("esp8266", Toolchain.ARDUINO): "esphome.arduino8266.toolchain",
("host", Toolchain.HOST): "esphome.host.toolchain",
("nrf52", Toolchain.SDK_NRF): "esphome.components.nrf52.toolchain",
}
@@ -40,6 +39,24 @@ def native_backend() -> ModuleType | None:
return importlib.import_module(module_path)
# Binutils and the linked image for memory analysis, for toolchains that build
# without PlatformIO but have no native build backend (which supplies them)
ANALYSIS_TOOLCHAIN_MODULES = {
("nrf52", Toolchain.SDK_NRF): "esphome.components.nrf52.toolchain",
}
def analysis_backend() -> ModuleType | None:
"""The module giving objdump, readelf and the ELF of a non-PlatformIO build.
None means PlatformIO's idedata supplies them (or nothing can).
"""
if (native := native_backend()) is not None:
return native
module_path = ANALYSIS_TOOLCHAIN_MODULES.get((CORE.target_platform, CORE.toolchain))
return importlib.import_module(module_path) if module_path else None
def warn_ignored_platformio_options(consumed: Collection[str]) -> None:
"""Warn for component-added platformio options a native build drops.
+1 -27
View File
@@ -33,35 +33,9 @@ _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}{_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
return CORE.data_dir / "storage" / f"{config_filename}.validated.json"
def save_compiled_config(config: ConfigType) -> None:
+3 -1
View File
@@ -297,7 +297,9 @@ void ADE7880::init_device_() {
void ADE7880::reset_device_() {
if (this->reset_pin_ != nullptr) {
ESP_LOGD(TAG, "Reset device using RESET pin");
pulse_reset(this->reset_pin_, {.low_ms = 1});
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
} else {
ESP_LOGD(TAG, "Reset device using SWRST command");
this->write_u16_register16_(CONFIG, CONFIG_SWRST);
+73 -73
View File
@@ -14,80 +14,80 @@ ESPHOME_LOG_TAG(TAG, "aic3204");
return; \
}
static constexpr i2c::I2CRegValue AIC3204_INIT[] PROGMEM = {
// Set register page to 0
{AIC3204_PAGE_CTRL, 0x00},
// Initiate SW reset (PLL is powered off as part of reset)
{AIC3204_SW_RST, 0x01},
// *** Program clock settings ***
// Default is CODEC_CLKIN is from MCLK pin. Don't need to change this.
// MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio)
// (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf)
// We do need MDAC*DOSR/32 >= the resource compute level for the processing block
// So here 2*128/32 = 8, which is equal to processing block 1 's resource compute
// See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow
// for determining these settings.
// Power up NDAC and set to 2
{AIC3204_NDAC, 0x82},
// Power up MDAC and set to 2
{AIC3204_MDAC, 0x82},
// Program DOSR = 128
{AIC3204_DOSR, 0x80},
// Set Audio Interface Config: I2S, 32 bits, DOUT always driving
{AIC3204_CODEC_IF, 0x30},
// For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In
{AIC3204_SCLK_MFP3, 0x02},
{AIC3204_AUDIO_IF_4, 0x01},
{AIC3204_AUDIO_IF_5, 0x01},
// Program the DAC processing block to be used - PRB_P1
{AIC3204_DAC_SIG_PROC, 0x01},
// *** Select Page 1 ***
{AIC3204_PAGE_CTRL, 0x01},
// Enable the internal AVDD_LDO:
{AIC3204_LDO_CTRL, 0x09},
// *** Program Analog Blocks ***
// Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO
{AIC3204_PWR_CFG, 0x08},
// Enable Master Analog Power Control
{AIC3204_LDO_CTRL, 0x01},
// Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v
// We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123)
// Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware
// We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47)
// (All pages refer to the TLV320AIC3204 Application Reference Guide)
{AIC3204_CM_CTRL, 0x40},
// *** Set PowerTune Modes ***
// Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver.
{AIC3204_PLAY_CFG1, 0x00},
{AIC3204_PLAY_CFG2, 0x00},
// Set the REF charging time to 40ms
{AIC3204_REF_STARTUP, 0x01},
// HP soft stepping settings for optimal pop performance at power up
// Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling
// capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound.
{AIC3204_HP_START, 0x25},
// Route Left DAC to HPL
{AIC3204_HPL_ROUTE, 0x08},
// Route Right DAC to HPR
{AIC3204_HPR_ROUTE, 0x08},
// Route Left DAC to LOL
{AIC3204_LOL_ROUTE, 0x08},
// Route Right DAC to LOR
{AIC3204_LOR_ROUTE, 0x08},
// Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
{AIC3204_HPL_GAIN, 0x3e},
// Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
{AIC3204_HPR_GAIN, 0x3e},
// Unmute LOL and set gain to 0dB
{AIC3204_LOL_DRV_GAIN, 0x00},
// Unmute LOR and set gain to 0dB
{AIC3204_LOR_DRV_GAIN, 0x00},
// Power up HPL and HPR, LOL and LOR drivers
{AIC3204_OP_PWR_CTRL, 0x3C},
};
void AIC3204::setup() {
ERROR_CHECK(this->write_registers(AIC3204_INIT), "Failed to initialize");
// Set register page to 0
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed");
// Initiate SW reset (PLL is powered off as part of reset)
ERROR_CHECK(this->write_byte(AIC3204_SW_RST, 0x01), "Software reset failed");
// *** Program clock settings ***
// Default is CODEC_CLKIN is from MCLK pin. Don't need to change this.
// MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio)
// (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf)
// We do need MDAC*DOSR/32 >= the resource compute level for the processing block
// So here 2*128/32 = 8, which is equal to processing block 1 's resource compute
// See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow
// for determining these settings.
// Power up NDAC and set to 2
ERROR_CHECK(this->write_byte(AIC3204_NDAC, 0x82), "Set NDAC failed");
// Power up MDAC and set to 2
ERROR_CHECK(this->write_byte(AIC3204_MDAC, 0x82), "Set MDAC failed");
// Program DOSR = 128
ERROR_CHECK(this->write_byte(AIC3204_DOSR, 0x80), "Set DOSR failed");
// Set Audio Interface Config: I2S, 32 bits, DOUT always driving
ERROR_CHECK(this->write_byte(AIC3204_CODEC_IF, 0x30), "Set CODEC_IF failed");
// For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In
ERROR_CHECK(this->write_byte(AIC3204_SCLK_MFP3, 0x02), "Set SCLK/MFP3 failed");
ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_4, 0x01), "Set AUDIO_IF_4 failed");
ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_5, 0x01), "Set AUDIO_IF_5 failed");
// Program the DAC processing block to be used - PRB_P1
ERROR_CHECK(this->write_byte(AIC3204_DAC_SIG_PROC, 0x01), "Set DAC_SIG_PROC failed");
// *** Select Page 1 ***
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x01), "Set page 1 failed");
// Enable the internal AVDD_LDO:
ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x09), "Set LDO_CTRL failed");
// *** Program Analog Blocks ***
// Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO
ERROR_CHECK(this->write_byte(AIC3204_PWR_CFG, 0x08), "Set PWR_CFG failed");
// Enable Master Analog Power Control
ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x01), "Set LDO_CTRL failed");
// Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v
// We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123)
// Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware
// We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47)
// (All pages refer to the TLV320AIC3204 Application Reference Guide)
ERROR_CHECK(this->write_byte(AIC3204_CM_CTRL, 0x40), "Set CM_CTRL failed");
// *** Set PowerTune Modes ***
// Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver.
ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG1, 0x00), "Set PLAY_CFG1 failed");
ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG2, 0x00), "Set PLAY_CFG2 failed");
// Set the REF charging time to 40ms
ERROR_CHECK(this->write_byte(AIC3204_REF_STARTUP, 0x01), "Set REF_STARTUP failed");
// HP soft stepping settings for optimal pop performance at power up
// Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling
// capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound.
ERROR_CHECK(this->write_byte(AIC3204_HP_START, 0x25), "Set HP_START failed");
// Route Left DAC to HPL
ERROR_CHECK(this->write_byte(AIC3204_HPL_ROUTE, 0x08), "Set HPL_ROUTE failed");
// Route Right DAC to HPR
ERROR_CHECK(this->write_byte(AIC3204_HPR_ROUTE, 0x08), "Set HPR_ROUTE failed");
// Route Left DAC to LOL
ERROR_CHECK(this->write_byte(AIC3204_LOL_ROUTE, 0x08), "Set LOL_ROUTE failed");
// Route Right DAC to LOR
ERROR_CHECK(this->write_byte(AIC3204_LOR_ROUTE, 0x08), "Set LOR_ROUTE failed");
// Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
ERROR_CHECK(this->write_byte(AIC3204_HPL_GAIN, 0x3e), "Set HPL_GAIN failed");
// Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
ERROR_CHECK(this->write_byte(AIC3204_HPR_GAIN, 0x3e), "Set HPR_GAIN failed");
// Unmute LOL and set gain to 0dB
ERROR_CHECK(this->write_byte(AIC3204_LOL_DRV_GAIN, 0x00), "Set LOL_DRV_GAIN failed");
// Unmute LOR and set gain to 0dB
ERROR_CHECK(this->write_byte(AIC3204_LOR_DRV_GAIN, 0x00), "Set LOR_DRV_GAIN failed");
// Power up HPL and HPR, LOL and LOR drivers
ERROR_CHECK(this->write_byte(AIC3204_OP_PWR_CTRL, 0x3C), "Set OP_PWR_CTRL failed");
// Wait for 2.5 sec for soft stepping to take effect before attempting power-up
this->set_timeout(2500, [this]() {
+8 -4
View File
@@ -174,10 +174,14 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
CONF_SUPPORTS_RESPONSE = "supports_response"
SupportsResponseType = api_ns.namespace("enums").enum("SupportsResponseType")
SUPPORTS_RESPONSE_OPTIONS = SupportsResponseType.options(
"none", "optional", "only", "status", prefix="SUPPORTS_RESPONSE_"
)
# Enum values in api::enums namespace
enums_ns = api_ns.namespace("enums")
SUPPORTS_RESPONSE_OPTIONS = {
"none": enums_ns.SUPPORTS_RESPONSE_NONE,
"optional": enums_ns.SUPPORTS_RESPONSE_OPTIONAL,
"only": enums_ns.SUPPORTS_RESPONSE_ONLY,
"status": enums_ns.SUPPORTS_RESPONSE_STATUS,
}
def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
+29
View File
@@ -83,6 +83,7 @@ service APIConnection {
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
rpc sendspin_pairing_token(SendspinPairingTokenRequest) returns (SendspinPairingTokenResponse) {}
rpc get_yaml(GetYamlRequest) returns (void) {}
}
@@ -401,6 +402,11 @@ message ZWaveProxyCapabilities {
uint32 home_id = 2;
}
message StoreYamlCapabilities {
// Firmware embeds its YAML, so clients can request it with get_yaml
bool supported = 1;
}
message WizardCapabilities {
// True when the device has a wizard configured, so DeviceWizardRequest will be answered
bool configured = 1;
@@ -425,6 +431,7 @@ message DeviceCapabilitiesResponse {
[(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
WizardCapabilities wizard = 5 [(field_ifdef) = "USE_API_WIZARD"];
SendspinCapabilities sendspin = 6 [(field_ifdef) = "USE_SENDSPIN"];
StoreYamlCapabilities store_yaml = 7 [(field_ifdef) = "USE_STORE_YAML"];
}
// ==================== DEVICE WIZARD ====================
@@ -3143,3 +3150,25 @@ message SendspinPairingTokenResponse {
// The pairing token (Sendspin pairing.md "Pairing Token"). Set only when status is OK.
string token = 2 [(max_data_length) = 127];
}
// ==================== STORE YAML ====================
// Streams the compressed user YAML embedded in firmware; the client decompresses.
// A GetYamlRequest during an active transfer is ignored; reconnect to restart.
message GetYamlRequest {
option (id) = 161;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_STORE_YAML";
option (no_delay) = true;
}
message GetYamlResponse {
option (id) = 162;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_STORE_YAML";
// A zstd frame of the EHY1 file envelope, sent from flash
bytes data = 1 [(force) = true, (pointer_to_buffer) = true, (progmem) = true];
bool done = 2 [(force) = true];
// Set on the first chunk only
uint32 total_size = 3;
}
+45 -1
View File
@@ -366,6 +366,13 @@ void APIConnection::loop() {
// (missing a frame is fine, missing a state update is not)
this->try_send_camera_image_();
#endif
#ifdef USE_STORE_YAML
// Guard inline so the idle hot path pays a compare, not a function call.
if (this->store_yaml_pos_ != STORE_YAML_IDLE) {
this->try_send_store_yaml_();
}
#endif
}
void APIConnection::check_keepalive_(uint32_t now) {
@@ -1222,6 +1229,36 @@ void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
}
#endif
#ifdef USE_STORE_YAML
void APIConnection::on_get_yaml_request() {
// A re-request while a transfer is in flight is ignored (see api.proto)
if (this->store_yaml_pos_ != STORE_YAML_IDLE)
return;
this->store_yaml_pos_ = 0;
this->try_send_store_yaml_();
}
void APIConnection::try_send_store_yaml_() {
// Advance only after a successful send, so a full TX buffer retries the same chunk from loop()
while (this->helper_->can_write_without_blocking()) {
const size_t to_send = std::min(STORE_YAML_DATA_SIZE - this->store_yaml_pos_, MAX_BATCH_PACKET_SIZE);
GetYamlResponse resp;
resp.data = STORE_YAML_DATA + this->store_yaml_pos_;
resp.data_len = to_send;
if (this->store_yaml_pos_ == 0)
resp.total_size = STORE_YAML_DATA_SIZE;
resp.done = this->store_yaml_pos_ + to_send == STORE_YAML_DATA_SIZE;
if (!this->send_message(resp))
return;
if (resp.done) {
this->store_yaml_pos_ = STORE_YAML_IDLE;
return;
}
this->store_yaml_pos_ += to_send;
}
}
#endif
#ifdef USE_HOMEASSISTANT_TIME
void APIConnection::on_get_time_response(const GetTimeResponse &value) {
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -2122,6 +2159,9 @@ bool APIConnection::send_device_capabilities_response_() {
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
resp.sendspin.client_id = sendspin_::global_sendspin_hub->get_client_id();
#endif
#ifdef USE_STORE_YAML
resp.store_yaml.supported = true;
#endif
return this->send_message(resp);
}
@@ -2364,10 +2404,14 @@ bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) {
bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn,
const void *msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise)
// Skip dump for log messages (recursive logging risk), camera frames (high-frequency noise)
// and YAML chunks (may contain secrets)
if (message_type != SubscribeLogsResponse::MESSAGE_TYPE
#ifdef USE_CAMERA
&& message_type != CameraImageResponse::MESSAGE_TYPE
#endif
#ifdef USE_STORE_YAML
&& message_type != GetYamlResponse::MESSAGE_TYPE
#endif
) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
+15
View File
@@ -32,6 +32,11 @@ namespace esphome::api {
// Forward-declared to break the api_server.h cycle; full-type inlines are in api_connection_buffer.h.
class APIServer;
#ifdef USE_STORE_YAML
/// The zstd YAML envelope generated by store_yaml, STORE_YAML_DATA_SIZE bytes in flash.
extern const uint8_t STORE_YAML_DATA[] PROGMEM;
#endif
// One shared flash string for every refused-frame warning: send_message()
// fails as soon as the TCP buffer is full, and each caller only pays for its
// short name. The guard drops the helper and its arguments below WARN.
@@ -123,6 +128,9 @@ class APIConnection final : public APIServerConnectionBase {
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
void on_camera_image_request(const CameraImageRequest &msg);
#endif
#ifdef USE_STORE_YAML
void on_get_yaml_request();
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
void on_climate_command_request(const ClimateCommandRequest &msg);
@@ -421,6 +429,13 @@ class APIConnection final : public APIServerConnectionBase {
void try_send_camera_image_();
#endif
#ifdef USE_STORE_YAML
void try_send_store_yaml_();
static constexpr size_t STORE_YAML_IDLE = std::numeric_limits<size_t>::max();
// Streaming offset into STORE_YAML_DATA, or STORE_YAML_IDLE
size_t store_yaml_pos_{STORE_YAML_IDLE};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_();
#endif
+37
View File
@@ -299,6 +299,19 @@ uint32_t ZWaveProxyCapabilities::calc_size_msg(const void *self) {
return size;
}
#endif
#ifdef USE_STORE_YAML
uint8_t *StoreYamlCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const StoreYamlCapabilities *>(self);
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.supported);
return pos;
}
uint32_t StoreYamlCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const StoreYamlCapabilities *>(self);
uint32_t size = 0;
size += ProtoSize::calc_bool(1, msg.supported);
return size;
}
#endif
#ifdef USE_API_WIZARD
uint8_t *WizardCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const WizardCapabilities *>(self);
@@ -348,6 +361,9 @@ uint8_t *DeviceCapabilitiesResponse::encode_msg(const void *self, uint8_t *__res
#endif
#ifdef USE_SENDSPIN
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.sendspin);
#endif
#ifdef USE_STORE_YAML
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.store_yaml);
#endif
return pos;
}
@@ -373,6 +389,9 @@ uint32_t DeviceCapabilitiesResponse::calc_size_msg(const void *self) {
#endif
#ifdef USE_SENDSPIN
size += ProtoSize::calc_message(1, msg.sendspin.calculate_size());
#endif
#ifdef USE_STORE_YAML
size += ProtoSize::calc_message(1, msg.store_yaml.calculate_size());
#endif
return size;
}
@@ -4407,6 +4426,24 @@ uint32_t SendspinPairingTokenResponse::calc_size_msg(const void *self) {
return size;
}
#endif
#ifdef USE_STORE_YAML
uint8_t *GetYamlResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const GetYamlResponse *>(self);
pos = ProtoEncode::encode_progmem_bytes_force(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.data, msg.data_len);
pos = ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
pos = ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, msg.done ? 0x01 : 0x00);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.total_size);
return pos;
}
uint32_t GetYamlResponse::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const GetYamlResponse *>(self);
uint32_t size = 0;
size += ProtoSize::calc_length_force(1, msg.data_len);
size += ProtoSize::calc_bool_force(1);
size += ProtoSize::calc_uint32(1, msg.total_size);
return size;
}
#endif
#ifndef HAS_PROTO_MESSAGE_DUMP
static_assert(!std::is_polymorphic_v<HelloRequest>, "decodable messages carry no vtable");
static_assert(!std::is_polymorphic_v<DisconnectRequest>, "decodable messages carry no vtable");
+46 -1
View File
@@ -734,6 +734,23 @@ class ZWaveProxyCapabilities final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_STORE_YAML
class StoreYamlCapabilities final : public ProtoMessage {
public:
bool supported{false};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_API_WIZARD
class WizardCapabilities final : public ProtoMessage {
public:
@@ -772,7 +789,7 @@ class SendspinCapabilities final : public ProtoMessage {
class DeviceCapabilitiesResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 150;
static constexpr uint8_t ESTIMATED_SIZE = 136;
static constexpr uint8_t ESTIMATED_SIZE = 153;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("device_capabilities_response"); }
#endif
@@ -793,6 +810,9 @@ class DeviceCapabilitiesResponse final : public ProtoMessage {
#endif
#ifdef USE_SENDSPIN
SendspinCapabilities sendspin{};
#endif
#ifdef USE_STORE_YAML
StoreYamlCapabilities store_yaml{};
#endif
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -4209,5 +4229,30 @@ class SendspinPairingTokenResponse final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_STORE_YAML
class GetYamlResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 162;
static constexpr uint8_t ESTIMATED_SIZE = 25;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("get_yaml_response"); }
#endif
const uint8_t *data{nullptr};
uint16_t data_len{0};
bool done{false};
uint32_t total_size{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
} // namespace esphome::api
+21
View File
@@ -1127,6 +1127,13 @@ const char *ZWaveProxyCapabilities::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_STORE_YAML
const char *StoreYamlCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("StoreYamlCapabilities"));
dump_field(out, ESPHOME_PSTR("supported"), this->supported);
return out.c_str();
}
#endif
#ifdef USE_API_WIZARD
const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("WizardCapabilities"));
@@ -1175,6 +1182,11 @@ const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const {
out.append(2, ' ').append_p(ESPHOME_PSTR("sendspin")).append(": ");
this->sendspin.dump_to(out);
out.append("\n");
#endif
#ifdef USE_STORE_YAML
out.append(2, ' ').append_p(ESPHOME_PSTR("store_yaml")).append(": ");
this->store_yaml.dump_to(out);
out.append("\n");
#endif
return out.c_str();
}
@@ -3016,6 +3028,15 @@ const char *SendspinPairingTokenResponse::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_STORE_YAML
const char *GetYamlResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("GetYamlResponse"));
dump_progmem_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
dump_field(out, ESPHOME_PSTR("done"), this->done);
dump_field(out, ESPHOME_PSTR("total_size"), this->total_size);
return out.c_str();
}
#endif
} // namespace esphome::api
@@ -760,6 +760,15 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
this->on_sendspin_pairing_token_request();
break;
}
#endif
#ifdef USE_STORE_YAML
case 161 /* GetYamlRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_get_yaml_request"));
#endif
this->on_get_yaml_request();
break;
}
#endif
default:
break;
+4
View File
@@ -256,6 +256,10 @@ class APIServerConnectionBase {
#ifdef USE_SENDSPIN
void on_sendspin_pairing_token_request() {};
#endif
#ifdef USE_STORE_YAML
void on_get_yaml_request() {};
#endif
};
} // namespace esphome::api
+3 -4
View File
@@ -253,9 +253,8 @@ template<typename... Ts> class APIRespondAction final : public Action<Ts...> {
#endif
void play(const Ts &...x) override {
// Extract call_id from first argument - it's always first for optional/only/status modes.
// By reference: array args are non-copyable FixedVector views.
auto args = std::forward_as_tuple(x...);
// Extract call_id from first argument - it's always first for optional/only/status modes
auto args = std::make_tuple(x...);
uint32_t call_id = std::get<0>(args);
bool success = this->success_.value(x...);
@@ -309,7 +308,7 @@ template<typename... Ts> class APIUnregisterServiceCallAction final : public Act
void play(const Ts &...x) override {
// Extract call_id from first argument - same convention as APIRespondAction
auto args = std::forward_as_tuple(x...);
auto args = std::make_tuple(x...);
uint32_t call_id = std::get<0>(args);
if (call_id != 0) {
this->parent_->unregister_active_action_call(call_id);
+6 -7
View File
@@ -54,8 +54,7 @@ static const char AS7341_NAME[] PROGMEM = "AS7341";
static constexpr uint8_t AS7341_ADC_SLOTS = 6;
static constexpr uint8_t AS7341_SMUX_LEN = 20;
static constexpr uint8_t AS7341_SMUX_STEPS = 2;
static constexpr i2c::I2CRegValue AS7341_INIT[] PROGMEM = {
{0x70, 0x00}}; // CONFIG: SPM mode, INT_SEL and LED_SEL cleared
static const RegValue AS7341_INIT[] PROGMEM = {{0x70, 0x00}}; // CONFIG: SPM mode, INT_SEL and LED_SEL cleared
static const uint8_t AS7341_SMUX[] PROGMEM = {
0x30, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x50, 0x00, // step 0: F1 to F4, NIR, Clear
0x00, 0x00, 0x20, 0x04, 0x00, 0x30, 0x01, 0x50, 0x00, 0x06, // step 0, continued
@@ -151,8 +150,8 @@ static constexpr uint8_t AS7343_ADC_SLOTS = 18;
static constexpr uint8_t AS7343_SMUX_LEN = 10;
static const char TCS3448_NAME[] PROGMEM = "TCS3448";
// CFG6, FD_CFG0, CFG10, CFG8, CFG20 (auto_smux, 18 channels in 3 cycles), AGC_GAIN_MAX, FD_TIME_1, FD_TIME_2
static constexpr i2c::I2CRegValue AS7343_INIT[] PROGMEM = {{0xF5, 0x00}, {0xDF, 0xA1}, {0x65, 0xF2}, {0xC9, 0xC8},
{0xD6, 0x62}, {0xD7, 0x99}, {0xE0, 0x64}, {0xE2, 0x21}};
static const RegValue AS7343_INIT[] PROGMEM = {{0xF5, 0x00}, {0xDF, 0xA1}, {0x65, 0xF2}, {0xC9, 0xC8},
{0xD6, 0x62}, {0xD7, 0x99}, {0xE0, 0x64}, {0xE2, 0x21}};
static const uint8_t AS7343_SMUX[] PROGMEM = {0x00, 0x04, 0x65, 0x02, 0x00, 0x05, 0x00, 0x01, 0x00, 0x30,
0x00, 0x00, 0x60, 0x20, 0x04, 0x50, 0x03, 0x00, 0x01, 0x05,
0x05, 0x00, 0x60, 0x00, 0x30, 0x00, 0x40, 0x10, 0x20, 0x00};
@@ -231,7 +230,7 @@ static constexpr ChipInfo as7343_chip(const char *name) {
.gain_correction = AS7343_GAIN_CORRECTION,
.contributions = AS7343_CONTRIBUTIONS,
.tristimulus = AS7343_TRISTIMULUS,
.init_count = sizeof(AS7343_INIT) / sizeof(i2c::I2CRegValue),
.init_count = sizeof(AS7343_INIT) / sizeof(RegValue),
.astep_reg = 0xD4,
.cfg0_reg = 0xBF,
.cfg1_reg = 0xC6,
@@ -299,9 +298,9 @@ void AS734XComponent::setup() {
this->mark_failed(LOG_STR("Wrong chip ID"));
return;
}
const i2c::I2CRegValue *init = pgm(chip->init);
const RegValue *init = pgm(chip->init);
for (uint8_t i = 0, n = pgm(chip->init_count); ok && i < n; i++) {
i2c::I2CRegValue rv;
RegValue rv;
progmem_memcpy(&rv, &init[i], sizeof(rv));
ok = this->select_bank_(rv.reg) && this->write_byte(rv.reg, rv.value);
}
+6 -1
View File
@@ -68,10 +68,15 @@ struct ChannelTristimulus {
float z;
};
struct RegValue {
uint8_t reg;
uint8_t value;
};
/// Per-chip constants, stored in flash. The pointed-to tables are in flash too.
struct ChipInfo {
const char *name;
const i2c::I2CRegValue *init; // written in order; the bank is selected per register address
const RegValue *init; // written in order; the bank is selected per register address
const uint8_t *smux; // AS7341: SMUX RAM image per step; AS7343: the three chain images
const uint8_t *readout; // per step, the ADC slot of each band (0xFF: not read in this step)
const uint16_t *wavelengths; // per band, WIDEBAND_NM for clear and ABSENT_NM for a missing band
@@ -16,7 +16,13 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a,
}
void AXS15231Touchscreen::setup() {
pulse_reset(this->reset_pin_, {.low_ms = 5, .high_after_ms = 10});
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
delay(10);
}
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
this->interrupt_pin_->setup();
-4
View File
@@ -78,7 +78,3 @@ async def pin_to_code(config: ConfigType) -> MockObj:
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
# Called by the OTA upload; the shared libretiny implementation.
ota_image = libretiny.device_ota_image
@@ -1,7 +1,7 @@
#include "bme680_bsec.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <string>
namespace esphome::bme680_bsec {
#ifdef USE_BSEC
@@ -9,7 +9,7 @@ ESPHOME_LOG_TAG(TAG, "bme680_bsec.sensor");
static constexpr uint32_t READ_TIMEOUT_ID = 0;
PROGMEM_STRING_TABLE(IaqAccuracyStrings, "Stabilizing", "Uncertain", "Calibrating", "Calibrated");
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
std::vector<BME680BSECComponent *>
BME680BSECComponent::instances; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -386,11 +386,10 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out
switch (outputs[i].sensor_id) {
case BSEC_OUTPUT_IAQ:
case BSEC_OUTPUT_STATIC_IAQ: {
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, IaqAccuracyStrings::LAST_INDEX);
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, std::size(IAQ_ACCURACY_STATES) - 1);
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_push_([this, accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_,
IaqAccuracyStrings::get_progmem_str(accuracy, IaqAccuracyStrings::LAST_INDEX));
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
});
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
@@ -436,9 +435,8 @@ void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, b
sensor->publish_state(value);
}
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, ProgmemStr value) {
if (!sensor ||
(sensor->has_state() && ESPHOME_strcmp_P(sensor->state.c_str(), reinterpret_cast<ESPHOME_PGM_P>(value)) == 0)) {
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
if (!sensor || (sensor->has_state() && sensor->state == value)) {
return;
}
sensor->publish_state(value);
+1 -1
View File
@@ -77,7 +77,7 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
int64_t get_time_ns_();
void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
void publish_sensor_(text_sensor::TextSensor *sensor, ProgmemStr value);
void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
void snapshot_state_(); // Fetch the current BSEC library state and save it in the bsec_state_data_ member (volatile
// memory)
+4 -7
View File
@@ -2,7 +2,6 @@
#include "cc1101pa.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cmath>
#include <cstring>
@@ -15,9 +14,6 @@ namespace esphome::cc1101 {
ESPHOME_LOG_TAG(TAG, "cc1101");
static constexpr size_t TX_FIFO_SIZE = 64;
// MDMCFG2 MOD_FORMAT values
PROGMEM_STRING_TABLE(ModulationStrings, "2-FSK", "GFSK", "UNUSED", "ASK/OOK", "4-FSK", "UNUSED", "UNUSED", "MSK");
static void split_float(float value, int mbits, uint8_t &e, uint32_t &m) {
int e_tmp;
float m_tmp = std::frexp(value, &e_tmp);
@@ -242,6 +238,8 @@ void CC1101Component::loop() {
}
void CC1101Component::dump_config() {
static const char *const MODULATION_NAMES[] = {"2-FSK", "GFSK", "UNUSED", "ASK/OOK",
"4-FSK", "UNUSED", "UNUSED", "MSK"};
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
XTAL_FREQUENCY / (1 << 16);
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
@@ -255,9 +253,8 @@ void CC1101Component::dump_config() {
" Symbol Rate: %.0f baud\n"
" Filter Bandwidth: %.1f Hz\n"
" Output Power: %.1f dBm",
this->chip_id_, freq, this->state_.CHANNR,
LOG_STR_ARG(ModulationStrings::get_log_str(this->state_.MOD_FORMAT & 0x07, 0)), symbol_rate, bw,
this->output_power_effective_);
this->chip_id_, freq, this->state_.CHANNR, MODULATION_NAMES[this->state_.MOD_FORMAT & 0x07],
symbol_rate, bw, this->output_power_effective_);
LOG_PIN(" CS Pin: ", this->cs_);
}
+3 -2
View File
@@ -45,8 +45,9 @@ void CopyCover::control(const cover::CoverCall &call) {
auto position = call.get_position();
if (position.has_value())
call2.set_position(*position);
if (call.get_toggle().has_value())
call2.set_command_toggle();
auto tilt2 = call.get_tilt();
if (tilt2.has_value())
call2.set_tilt(*tilt2);
call2.perform();
}
@@ -21,6 +21,4 @@ void CopyLock::control(const lock::LockCall &call) {
call2.perform();
}
void CopyLock::open_latch() { this->source_->open(); }
} // namespace esphome::copy
-1
View File
@@ -13,7 +13,6 @@ class CopyLock final : public lock::Lock, public Component {
protected:
void control(const lock::LockCall &call) override;
void open_latch() override;
lock::Lock *source_;
};
@@ -6,7 +6,12 @@ ESPHOME_LOG_TAG(TAG, "cst226.touchscreen");
void CST226Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
this->set_timeout(30, [this] { this->continue_setup_(); });
} else {
this->continue_setup_();
@@ -43,7 +43,9 @@ void CST328Touchscreen::setup() {
}
void CST328Touchscreen::reset_device_() {
pulse_reset(this->reset_pin_, {.low_ms = 5});
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
this->set_timeout(CST328_TRANSITION_TIMEOUT, [this] { this->continue_setup_(); });
}
@@ -52,7 +52,12 @@ void CST816Touchscreen::continue_setup_() {
void CST816Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
this->set_timeout(30, [this] { this->continue_setup_(); });
} else {
this->continue_setup_();
@@ -10,7 +10,14 @@ static constexpr uint32_t REREAD_TIMEOUT_ID = 1;
static constexpr uint32_t REREAD_DELAY_MS = 20;
void CST9220Touchscreen::setup() {
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 10});
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
// Wait for the controller to leave its bootloader before talking to it.
this->set_timeout(30, [this] { this->continue_setup_(); });
}
-1
View File
@@ -57,7 +57,6 @@ class DemoCover final : public cover::Cover, public Component {
}
if (call.get_stop()) {
this->cancel_timeout(MOVE_TIMEOUT_ID);
this->current_operation = cover::COVER_OPERATION_IDLE;
}
this->publish_state();
@@ -42,7 +42,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_LINE_READER")
await uart.new_uart_component(config)
@@ -11,10 +11,10 @@
namespace esphome::dfrobot_sen0395 {
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395");
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;
// Sent after a reset with no line end
static constexpr char PROMPT[] PROGMEM = "leapMMW:/>";
// 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",
@@ -121,9 +121,9 @@ bool DfrobotSen0395Component::run_command_(const Command &cmd, uint32_t now) {
return false; // Command not done yet
}
if (const char *message = this->read_message_()) {
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("is not recognized as a CLI command")) == nullptr) {
if (!this->handle_reply_(cmd, message))
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;
@@ -146,13 +146,12 @@ bool DfrobotSen0395Component::run_command_(const Command &cmd, uint32_t now) {
}
void DfrobotSen0395Component::read_state_() {
const char *message = this->read_message_();
if (message == nullptr)
if (!this->read_message_())
return;
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("$JYBSS,0, , , *")) != nullptr) {
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(message, ESPHOME_PSTR("$JYBSS,1, , , *")) != nullptr) {
} else if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("$JYBSS,1, , , *")) != nullptr) {
this->set_detected_(true);
this->set_active(true);
}
@@ -207,7 +206,8 @@ void DfrobotSen0395Component::format_command(const Command &cmd, char *buf, size
}
}
bool DfrobotSen0395Component::handle_reply_(const Command &cmd, const char *message) {
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
@@ -230,7 +230,7 @@ bool DfrobotSen0395Component::handle_reply_(const Command &cmd, const char *mess
}
return true;
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
if (!is(PROMPT))
if (!is(ESPHOME_PSTR("leapMMW:/>")))
return false;
ESP_LOGI(TAG, "Restarted sensor.");
return true;
@@ -300,24 +300,34 @@ bool DfrobotSen0395Component::handle_reply_(const Command &cmd, const char *mess
return true;
}
const char *DfrobotSen0395Component::read_message_() {
bool DfrobotSen0395Component::read_message_() {
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
if (this->read_pos_ == MMWAVE_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte);
if (byte == ASCII_CR)
continue;
if (byte >= 0x7F)
byte = '?'; // needs to be valid utf8 string for log functions.
const char *line = this->line_reader_.feed(byte);
if (line == nullptr && byte == '>' && this->line_reader_.length() == sizeof(PROMPT) - 1) {
line = this->line_reader_.partial();
this->line_reader_.reset();
this->read_buffer_[this->read_pos_] = byte;
if (this->read_pos_ == 9 && byte == '>')
this->read_buffer_[++this->read_pos_] = ASCII_LF;
if (this->read_buffer_[this->read_pos_] == ASCII_LF) {
this->read_buffer_[this->read_pos_] = 0;
this->read_pos_ = 0;
ESP_LOGV(TAG, "Message: %s", this->read_buffer_);
return true; // Full message in buffer
}
if (line != nullptr) {
ESP_LOGV(TAG, "Message: %s", line);
return line;
}
this->line_reader_.log_if_overflowed(TAG);
this->read_pos_++;
}
return nullptr;
return false; // No full message yet
}
void DfrobotSen0395Component::set_detected_(bool detected) {
@@ -4,7 +4,6 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/line_reader.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
@@ -15,7 +14,7 @@
namespace esphome::dfrobot_sen0395 {
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 253; // largest LineReader size with a one byte position
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
static constexpr uint8_t COMMAND_QUEUE_SIZE = 20;
// Keep in sync with the BusyActions table in dfrobot_sen0395.cpp
@@ -122,7 +121,8 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
bool led_active_{false};
bool uart_presence_active_{false};
bool start_after_boot_{false};
LineReader<MMWAVE_READ_BUFFER_LENGTH> line_reader_;
char read_buffer_[MMWAVE_READ_BUFFER_LENGTH];
size_t read_pos_{0};
bool cmd_sent_{false};
uint8_t retries_left_{2};
StaticRingBuffer<Command, COMMAND_QUEUE_SIZE> cmd_queue_;
@@ -131,13 +131,12 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
int16_t det_range_steps_[8]{};
uint16_t latency_steps_[2]{};
/// The next complete line, valid until the next call, or nullptr
const char *read_message_();
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, const char *message);
bool handle_reply_(const Command &cmd);
void set_detected_(bool detected);
};
+4 -15
View File
@@ -1,5 +1,4 @@
from dataclasses import dataclass
import logging
from typing import Any, Protocol
from esphome import automation, core, pins
@@ -35,8 +34,6 @@ from esphome.types import ConfigType
DOMAIN = "display"
IS_PLATFORM_COMPONENT = True
_LOGGER = logging.getLogger(__name__)
display_ns = cg.esphome_ns.namespace("display")
Display = display_ns.class_("Display", cg.PollingComponent)
DisplayBuffer = display_ns.class_("DisplayBuffer", Display)
@@ -105,21 +102,13 @@ BASIC_DISPLAY_SCHEMA = cv.Schema(
).extend(cv.polling_component_schema("1s"))
def _validate_test_card(config: ConfigType) -> ConfigType:
def _validate_test_card(config):
if (
config.get(CONF_SHOW_TEST_CARD, False)
and (update_interval := config.get(CONF_UPDATE_INTERVAL)) is not None
and update_interval.total_milliseconds == SCHEDULER_DONT_RUN
and config.get(CONF_UPDATE_INTERVAL, False) == SCHEDULER_DONT_RUN
):
# Only a warning: something else can still update the display, for example
# component.update, and the test card is drawn then.
display_id = config.get(CONF_ID)
_LOGGER.warning(
"%s`%s: True` is used with `%s: never`, so the test card is only drawn "
"when something updates the display (for example component.update)",
f"{display_id.id}: " if display_id is not None and display_id.id else "",
CONF_SHOW_TEST_CARD,
CONF_UPDATE_INTERVAL,
raise cv.Invalid(
f"`{CONF_SHOW_TEST_CARD}: True` cannot be used with `{CONF_UPDATE_INTERVAL}: never` because this combination will not show a test_card."
)
return config
+1 -1
View File
@@ -22,7 +22,7 @@ CONF_CUSTOM_PATTERNS = "custom_patterns"
CONF_SKIP_CRC = "skip_crc"
CONF_DEFAULT_OBIS = "default_obis"
DLMS_PARSER_VERSION = "2.2.2"
DLMS_PARSER_VERSION = "2.2.0"
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
DlmsMeterComponent = dlms_meter_component_ns.class_(
@@ -1,6 +1,5 @@
#include "ds248x_one_wire_bus.h"
#include "ds248x.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ds248x {
@@ -152,7 +151,11 @@ uint64_t DS248xOneWireBus::search_int() {
}
// Update address based on direction taken
assign_bits(address, bit_mask, dir_taken);
if (dir_taken) {
address |= bit_mask;
} else {
address &= ~bit_mask;
}
}
// Search successful
+6 -4
View File
@@ -18,11 +18,13 @@ E131AddressableLightEffect = e131_ns.class_(
)
E131Component = e131_ns.class_("E131Component", cg.Component)
E131ListenMethod = e131_ns.enum("E131ListenMethod")
METHODS = E131ListenMethod.options("UNICAST", "MULTICAST", prefix="E131_")
METHODS = {"UNICAST": e131_ns.E131_UNICAST, "MULTICAST": e131_ns.E131_MULTICAST}
E131LightChannels = e131_ns.enum("E131LightChannels")
CHANNELS = E131LightChannels.options("MONO", "RGB", "RGBW", prefix="E131_")
CHANNELS = {
"MONO": e131_ns.E131_MONO,
"RGB": e131_ns.E131_RGB,
"RGBW": e131_ns.E131_RGBW,
}
CONF_UNIVERSE = "universe"
CONF_E131_ID = "e131_id"
@@ -20,7 +20,9 @@ void EKTF2232Touchscreen::setup() {
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
pulse_reset(this->reset_pin_, {.low_ms = 15, .high_after_ms = 15});
this->reset_pin_->setup();
this->hard_reset_();
if (!this->soft_reset_()) {
ESP_LOGE(TAG, "Failed to soft reset EKT2232!");
this->interrupt_pin_->detach_interrupt();
@@ -94,6 +96,13 @@ bool EKTF2232Touchscreen::get_power_state() {
return (received[1] >> 3) & 1;
}
void EKTF2232Touchscreen::hard_reset_() {
this->reset_pin_->digital_write(false);
delay(15);
this->reset_pin_->digital_write(true);
delay(15);
}
bool EKTF2232Touchscreen::soft_reset_() {
auto err = this->write(SOFT_RESET_CMD, 4);
if (err != i2c::ERROR_OK)
@@ -22,6 +22,7 @@ class EKTF2232Touchscreen final : public Touchscreen, public i2c::I2CDevice {
bool get_power_state();
protected:
void hard_reset_();
bool soft_reset_();
void update_touches() override;
+15 -12
View File
@@ -4,18 +4,21 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi");
static constexpr size_t EPAPER_MAX_CMD_LOG_BYTES = 128;
PROGMEM_STRING_TABLE(EPaperStateStrings, "IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE",
"TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", "Unknown");
static constexpr const char *const EPAPER_STATE_STRINGS[] = {
"IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE",
"TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP",
};
const LogString *EPaperBase::epaper_state_to_string_() {
return EPaperStateStrings::get_log_str(static_cast<uint8_t>(this->state_), EPaperStateStrings::LAST_INDEX);
const char *EPaperBase::epaper_state_to_string_() {
if (auto idx = static_cast<unsigned>(this->state_); idx < std::size(EPAPER_STATE_STRINGS))
return EPAPER_STATE_STRINGS[idx];
return "Unknown";
}
void EPaperBase::setup() {
@@ -156,7 +159,7 @@ void EPaperBase::update_effective_transform_() {
void EPaperBase::update() {
if (this->state_ != EPaperState::IDLE) {
ESP_LOGE(TAG, "Display already in state %s", LOG_STR_ARG(epaper_state_to_string_()));
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
return;
}
this->set_state_(EPaperState::UPDATE);
@@ -209,7 +212,7 @@ void EPaperBase::loop() {
this->high_frequency_loop_.stop();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (now - this->waiting_for_idle_last_print_ >= 1000) {
ESP_LOGV(TAG, "Waiting for idle in state %s", LOG_STR_ARG(this->epaper_state_to_string_()));
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
this->waiting_for_idle_last_print_ = millis();
}
#endif
@@ -224,7 +227,7 @@ void EPaperBase::loop() {
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
void EPaperBase::abandon_update_() {
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
LOG_STR_ARG(this->epaper_state_to_string_()));
this->epaper_state_to_string_());
this->status_set_warning(LOG_STR("Busy pin timeout"));
if (this->state_ == EPaperState::TRANSFER_DATA)
this->abort_transfer();
@@ -244,10 +247,10 @@ void EPaperBase::abandon_update_() {
* Should a subclassed class need to override this, the method will need to be made virtual.
*/
void EPaperBase::process_state_() {
ESP_LOGV(TAG, "Process state entered in state %s", LOG_STR_ARG(epaper_state_to_string_()));
ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_());
switch (this->state_) {
default:
ESP_LOGE(TAG, "Display is in unhandled state %s", LOG_STR_ARG(epaper_state_to_string_()));
ESP_LOGE(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
this->set_state_(EPaperState::IDLE);
break;
case EPaperState::IDLE:
@@ -339,7 +342,7 @@ void EPaperBase::process_state_() {
}
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
ESP_LOGV(TAG, "Exit state %s", LOG_STR_ARG(this->epaper_state_to_string_()));
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
this->state_ = state;
// allow subclasses to nominate delays
if (delay == 0)
@@ -347,7 +350,7 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
this->next_delay_ = 0;
this->delay_until_ = millis() + delay;
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", LOG_STR_ARG(this->epaper_state_to_string_()), delay,
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
TRUEFALSE(this->waiting_for_idle_));
// Each step of an update otherwise waits for the next loop iteration; loop() slows down again for long waits
if (state == EPaperState::IDLE) {
+1 -1
View File
@@ -125,7 +125,7 @@ class EPaperBase : public Display,
bool is_using_partial_update_() const { return this->full_update_every_ > 1; }
void process_state_();
const LogString *epaper_state_to_string_();
const char *epaper_state_to_string_();
bool is_idle_() const;
void setup_pins_() const;
virtual bool reset();
@@ -72,7 +72,9 @@ bool EPaperJD79660::reset() {
// As commented on SLEEP_MS_RESET1: Reset pulse must happen within time window.
// So do not use FSM loop, and avoid other calls/logs during pulse below.
pulse_reset(this->reset_pin_, {.low_ms = SLEEP_MS_RESET1});
this->reset_pin_->digital_write(false);
delay(SLEEP_MS_RESET1);
this->reset_pin_->digital_write(true);
this->reset_duration_ = SLEEP_MS_RESET2;
this->step_ = FSMState::RESET_STEP2_IDLECHECK;
+36 -42
View File
@@ -33,59 +33,53 @@ void ES7210::dump_config() {
this->channel_gains_[2], this->channel_gains_[3]);
}
static constexpr i2c::I2CRegValue ES7210_RESET[] PROGMEM = {
// Software reset
{ES7210_RESET_REG00, 0xff},
{ES7210_RESET_REG00, 0x32},
{ES7210_CLOCK_OFF_REG01, 0x3f},
// Set initialization time when device powers up
{ES7210_TIME_CONTROL0_REG09, 0x30},
{ES7210_TIME_CONTROL1_REG0A, 0x30},
// Configure HFP for all ADC channels
{ES7210_ADC12_HPF2_REG23, 0x2a},
{ES7210_ADC12_HPF1_REG22, 0x0a},
{ES7210_ADC34_HPF2_REG20, 0x0a},
{ES7210_ADC34_HPF1_REG21, 0x2a},
};
static constexpr i2c::I2CRegValue ES7210_ANALOG[] PROGMEM = {
// Configure analog power
{ES7210_ANALOG_REG40, 0xC3},
// Set mic bias
{ES7210_MIC12_BIAS_REG41, 0x70},
{ES7210_MIC34_BIAS_REG42, 0x70},
};
static constexpr i2c::I2CRegValue ES7210_POWER_ON[] PROGMEM = {
// Power on mics 1 through 4
{ES7210_MIC1_POWER_REG47, 0x08},
{ES7210_MIC2_POWER_REG48, 0x08},
{ES7210_MIC3_POWER_REG49, 0x08},
{ES7210_MIC4_POWER_REG4A, 0x08},
// Power down DLL
{ES7210_POWER_DOWN_REG06, 0x04},
// Power on MIC1-4 bias & ADC1-4 & PGA1-4 Power
{ES7210_MIC12_POWER_REG4B, 0x0F},
{ES7210_MIC34_POWER_REG4C, 0x0F},
// Enable device
{ES7210_RESET_REG00, 0x71},
{ES7210_RESET_REG00, 0x41},
};
void ES7210::setup() {
ES7210_ERROR_FAILED(this->write_registers(ES7210_RESET));
// Software reset
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0xff));
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x32));
ES7210_ERROR_FAILED(this->write_byte(ES7210_CLOCK_OFF_REG01, 0x3f));
// Set initialization time when device powers up
ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL0_REG09, 0x30));
ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL1_REG0A, 0x30));
// Configure HFP for all ADC channels
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF2_REG23, 0x2a));
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF1_REG22, 0x0a));
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF2_REG20, 0x0a));
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF1_REG21, 0x2a));
// Secondary I2S mode settings
ES7210_ERROR_FAILED(this->update_register(ES7210_MODE_CONFIG_REG08, 0x01, 0x00));
ES7210_ERROR_FAILED(this->write_registers(ES7210_ANALOG));
// Configure analog power
ES7210_ERROR_FAILED(this->write_byte(ES7210_ANALOG_REG40, 0xC3));
// Set mic bias
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_BIAS_REG41, 0x70));
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_BIAS_REG42, 0x70));
// Configure I2S settings, sample rate, and microphone gains
ES7210_ERROR_FAILED(this->configure_i2s_format_());
ES7210_ERROR_FAILED(this->configure_sample_rate_());
ES7210_ERROR_FAILED(this->configure_mic_gain_());
ES7210_ERROR_FAILED(this->write_registers(ES7210_POWER_ON));
// Power on mics 1 through 4
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC1_POWER_REG47, 0x08));
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC2_POWER_REG48, 0x08));
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC3_POWER_REG49, 0x08));
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC4_POWER_REG4A, 0x08));
// Power down DLL
ES7210_ERROR_FAILED(this->write_byte(ES7210_POWER_DOWN_REG06, 0x04));
// Power on MIC1-4 bias & ADC1-4 & PGA1-4 Power
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x0F));
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x0F));
// Enable device
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x71));
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x41));
this->setup_complete_ = true;
}
+48 -54
View File
@@ -25,62 +25,56 @@ ESPHOME_LOG_TAG(TAG, "es7243e");
void ES7243E::dump_config() { ESP_LOGCONFIG(TAG, "ES7243E audio ADC:"); }
// clang-format off
static constexpr i2c::I2CRegValue ES7243E_INIT[] PROGMEM = {
{ES7243E_CLOCK_MGR_REG01, 0x3A},
{ES7243E_RESET_REG00, 0x80},
{ES7243E_TEST_MODE_REGF9, 0x00},
{ES7243E_CLOCK_MGR_REG04, 0x02},
{ES7243E_CLOCK_MGR_REG04, 0x01},
{ES7243E_TEST_MODE_REGF9, 0x01},
{ES7243E_RESET_REG00, 0x1E},
{ES7243E_CLOCK_MGR_REG01, 0x00},
{ES7243E_CLOCK_MGR_REG02, 0x00},
{ES7243E_CLOCK_MGR_REG03, 0x20},
{ES7243E_CLOCK_MGR_REG04, 0x01},
{ES7243E_ADC_CTRL_REG0D, 0x00},
{ES7243E_CLOCK_MGR_REG05, 0x00},
{ES7243E_CLOCK_MGR_REG06, 0x03}, // SCLK=MCLK/4
{ES7243E_CLOCK_MGR_REG07, 0x00}, // LRCK=MCLK/256
{ES7243E_CLOCK_MGR_REG08, 0xFF}, // LRCK=MCLK/256
{ES7243E_CLOCK_MGR_REG09, 0xCA},
{ES7243E_SDP_REG0A, 0x85},
{ES7243E_SDP_REG0B, 0x00},
{ES7243E_ADC_CTRL_REG0E, 0xBF},
{ES7243E_ADC_CTRL_REG0F, 0x80},
{ES7243E_ADC_CTRL_REG14, 0x0C},
{ES7243E_ADC_CTRL_REG15, 0x0C},
{ES7243E_ANALOG_REG17, 0x02},
{ES7243E_ANALOG_REG18, 0x26},
{ES7243E_ANALOG_REG19, 0x77},
{ES7243E_ANALOG_REG1A, 0xF4},
{ES7243E_ANALOG_REG1B, 0x66},
{ES7243E_ANALOG_REG1C, 0x44},
{ES7243E_ANALOG_REG1E, 0x00},
{ES7243E_ANALOG_REG1F, 0x0C},
{ES7243E_ANALOG_REG20, 0x1A}, // PGA gain +30dB
{ES7243E_ANALOG_REG21, 0x1A},
{ES7243E_RESET_REG00, 0x80},
{ES7243E_CLOCK_MGR_REG01, 0x3A},
{ES7243E_ANALOG_REG16, 0x3F},
{ES7243E_ANALOG_REG16, 0x00},
{ES7243E_TEST_MODE_REGF9, 0x00},
{ES7243E_CLOCK_MGR_REG04, 0x01},
{ES7243E_ANALOG_REG17, 0x01},
};
// clang-format on
static constexpr i2c::I2CRegValue ES7243E_START[] PROGMEM = {
{ES7243E_RESET_REG00, 0x80},
{ES7243E_CLOCK_MGR_REG01, 0x3A},
{ES7243E_ANALOG_REG16, 0x3F},
{ES7243E_ANALOG_REG16, 0x00},
};
void ES7243E::setup() {
ES7243E_ERROR_FAILED(this->write_registers(ES7243E_INIT));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x3A));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x80));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_TEST_MODE_REGF9, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x02));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x01));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_TEST_MODE_REGF9, 0x01));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x1E));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG02, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG03, 0x20));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x01));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG0D, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG05, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG06, 0x03)); // SCLK=MCLK/4
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG07, 0x00)); // LRCK=MCLK/256
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG08, 0xFF)); // LRCK=MCLK/256
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG09, 0xCA));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_SDP_REG0A, 0x85));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_SDP_REG0B, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG0E, 0xBF));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG0F, 0x80));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG14, 0x0C));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG15, 0x0C));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG17, 0x02));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG18, 0x26));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG19, 0x77));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1A, 0xF4));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1B, 0x66));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1C, 0x44));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1E, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1F, 0x0C));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG20, 0x1A)); // PGA gain +30dB
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG21, 0x1A));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x80));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x3A));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x3F));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_TEST_MODE_REGF9, 0x00));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x01));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG17, 0x01));
ES7243E_ERROR_FAILED(this->configure_mic_gain_());
ES7243E_ERROR_FAILED(this->write_registers(ES7243E_START));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x80));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x3A));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x3F));
ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x00));
this->setup_complete_ = true;
}
+62 -46
View File
@@ -9,54 +9,70 @@ namespace esphome::es8156 {
ESPHOME_LOG_TAG(TAG, "es8156");
static constexpr i2c::I2CRegValue ES8156_INIT[] PROGMEM = {
// REG02 MODE CONFIG 1: Enable software mode for I2C control of volume/mute
// Bit 2: SOFT_MODE_SEL=1 (software mode enabled)
{ES8156_REG02_SCLK_MODE, 0x04},
// Analog system configuration (active-low power down bits, active-high enables)
// REG20 ANALOG SYSTEM: Configure analog signal path
{ES8156_REG20_ANALOG_SYS1, 0x2A},
// REG21 ANALOG SYSTEM: VSEL=0x1C (bias level ~120%), normal VREF ramp speed
{ES8156_REG21_ANALOG_SYS2, 0x3C},
// REG22 ANALOG SYSTEM: Line out mode (HPSW=0), OUT_MUTE=0 (not muted)
{ES8156_REG22_ANALOG_SYS3, 0x00},
// REG24 ANALOG SYSTEM: Low power mode for VREFBUF, HPCOM, DACVRP; DAC normal power
// Bits 2:0 = 0x07: LPVREFBUF=1, LPHPCOM=1, LPDACVRP=1, LPDAC=0
{ES8156_REG24_ANALOG_LP, 0x07},
// REG23 ANALOG SYSTEM: Lowest bias (IBIAS_SW=0), VMIDLVL=VDDA/2, normal impedance
{ES8156_REG23_ANALOG_SYS4, 0x00},
// Timing and interface configuration
// REG0A/0B TIME CONTROL: Fast state machine transitions
{ES8156_REG0A_TIME_CONTROL1, 0x01},
{ES8156_REG0B_TIME_CONTROL2, 0x01},
// REG11 SDP INTERFACE CONFIG: Default I2S format (24-bit, I2S mode)
{ES8156_REG11_DAC_SDP, 0x00},
// REG19 EQ CONTROL 1: EQ disabled (EQ_ON=0), EQ_BAND_NUM=2
{ES8156_REG19_EQ_CONTROL1, 0x20},
// REG0D P2S CONTROL: Parallel-to-serial converter settings
{ES8156_REG0D_P2S_CONTROL, 0x14},
// REG09 MISC CONTROL 2: Default settings
{ES8156_REG09_MISC_CONTROL2, 0x00},
// REG18 MISC CONTROL 3: Stereo channel routing, no inversion
// Bits 5:4 CHN_CROSS: 0=L→L/R→R, 1=L to both, 2=R to both, 3=swap L/R
// Bits 3:2: LCH_INV/RCH_INV channel inversion
{ES8156_REG18_MISC_CONTROL3, 0x00},
// REG08 CLOCK OFF: Enable all internal clocks (0x3F = all clock gates open)
{ES8156_REG08_CLOCK_ON_OFF, 0x3F},
// REG00 RESET CONTROL: Reset sequence
// First: RST_DIG=1 (assert digital reset)
{ES8156_REG00_RESET, 0x02},
// Then: CSM_ON=1 (enable chip state machine), RST_DIG=1
{ES8156_REG00_RESET, 0x03},
// REG25 ANALOG SYSTEM: Power up analog blocks
// VMIDSEL=2 (normal VMID operation), PDN_ANA=0, ENREFR=0, ENHPCOM=0
// PDN_DACVREFGEN=0, PDN_VREFBUF=0, PDN_DAC=0 (all enabled)
{ES8156_REG25_ANALOG_SYS5, 0x20},
};
// Mark the component as failed; use only in setup
#define ES8156_ERROR_FAILED(func) \
if (!(func)) { \
this->mark_failed(LOG_STR("Failed to initialize")); \
return; \
}
void ES8156::setup() {
if (!this->write_registers(ES8156_INIT))
this->mark_failed(LOG_STR("Failed to initialize"));
// REG02 MODE CONFIG 1: Enable software mode for I2C control of volume/mute
// Bit 2: SOFT_MODE_SEL=1 (software mode enabled)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG02_SCLK_MODE, 0x04));
// Analog system configuration (active-low power down bits, active-high enables)
// REG20 ANALOG SYSTEM: Configure analog signal path
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG20_ANALOG_SYS1, 0x2A));
// REG21 ANALOG SYSTEM: VSEL=0x1C (bias level ~120%), normal VREF ramp speed
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG21_ANALOG_SYS2, 0x3C));
// REG22 ANALOG SYSTEM: Line out mode (HPSW=0), OUT_MUTE=0 (not muted)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG22_ANALOG_SYS3, 0x00));
// REG24 ANALOG SYSTEM: Low power mode for VREFBUF, HPCOM, DACVRP; DAC normal power
// Bits 2:0 = 0x07: LPVREFBUF=1, LPHPCOM=1, LPDACVRP=1, LPDAC=0
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG24_ANALOG_LP, 0x07));
// REG23 ANALOG SYSTEM: Lowest bias (IBIAS_SW=0), VMIDLVL=VDDA/2, normal impedance
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG23_ANALOG_SYS4, 0x00));
// Timing and interface configuration
// REG0A/0B TIME CONTROL: Fast state machine transitions
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0A_TIME_CONTROL1, 0x01));
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0B_TIME_CONTROL2, 0x01));
// REG11 SDP INTERFACE CONFIG: Default I2S format (24-bit, I2S mode)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG11_DAC_SDP, 0x00));
// REG19 EQ CONTROL 1: EQ disabled (EQ_ON=0), EQ_BAND_NUM=2
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG19_EQ_CONTROL1, 0x20));
// REG0D P2S CONTROL: Parallel-to-serial converter settings
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0D_P2S_CONTROL, 0x14));
// REG09 MISC CONTROL 2: Default settings
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG09_MISC_CONTROL2, 0x00));
// REG18 MISC CONTROL 3: Stereo channel routing, no inversion
// Bits 5:4 CHN_CROSS: 0=L→L/R→R, 1=L to both, 2=R to both, 3=swap L/R
// Bits 3:2: LCH_INV/RCH_INV channel inversion
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG18_MISC_CONTROL3, 0x00));
// REG08 CLOCK OFF: Enable all internal clocks (0x3F = all clock gates open)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG08_CLOCK_ON_OFF, 0x3F));
// REG00 RESET CONTROL: Reset sequence
// First: RST_DIG=1 (assert digital reset)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x02));
// Then: CSM_ON=1 (enable chip state machine), RST_DIG=1
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x03));
// REG25 ANALOG SYSTEM: Power up analog blocks
// VMIDSEL=2 (normal VMID operation), PDN_ANA=0, ENREFR=0, ENHPCOM=0
// PDN_DACVREFGEN=0, PDN_VREFBUF=0, PDN_DAC=0 (all enabled)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG25_ANALOG_SYS5, 0x20));
}
void ES8156::dump_config() { ESP_LOGCONFIG(TAG, "ES8156 Audio Codec:"); }
+78 -69
View File
@@ -22,76 +22,79 @@ ESPHOME_LOG_TAG(TAG, "es8388");
return false; \
}
static constexpr i2c::I2CRegValue ES8388_INIT[] PROGMEM = {
// I2S worker mode
{ES8388_MASTERMODE, 0x00},
/* Chip Control and Power Management */
{ES8388_CONTROL2, 0x50},
// normal all and power up all
{ES8388_CHIPPOWER, 0x00},
// vmidsel/500k
// EnRef=0,Play&Record Mode,(0x17-both of mic&play)
{ES8388_CONTROL1, 0x12},
// i2s 16 bits
{ES8388_DACCONTROL1, 0x18},
// sample freq 256
// DACFsMode,SINGLE SPEED; DACFsRatio,256
{ES8388_DACCONTROL2, 0x02},
// 0x00 audio on LIN1&RIN1, 0x09 LIN2&RIN2
{ES8388_DACCONTROL16, 0x00},
// only left DAC to left mixer enable 0db
{ES8388_DACCONTROL17, 0x90},
// only right DAC to right mixer enable 0db
{ES8388_DACCONTROL20, 0x90},
// set internal ADC and DAC use the same LRCK clock, ADC LRCK as internal LRCK
{ES8388_DACCONTROL21, 0x80},
// vroi=0 - 1.5k VREF to analog output resistance (default)
{ES8388_DACCONTROL23, 0x00},
// power down adc and line in
{ES8388_ADCPOWER, 0xFF},
//@nightdav
{ES8388_ADCCONTROL1, 0x00}, // +21dB : recommended value for ALC & voice recording
// set to Mono Right
{ES8388_ADCCONTROL3, 0x02},
// I2S 16 Bits length and I2S serial audio data format
{ES8388_ADCCONTROL4, 0x0d},
// ADCFsMode,singel SPEED,RATIO=256
{ES8388_ADCCONTROL5, 0x02},
// ADC Volume
{ES8388_ADCCONTROL8, 0x00},
{ES8388_ADCCONTROL9, 0x00},
//@nightDav
// ALC Config (as recommended by ES8388 user guide for voice recording)
// Reg 0x12 = 0xe2 (ALC enable, PGA Max. Gain=23.5dB, Min. Gain=0dB)
{ES8388_ADCCONTROL10, 0xe2},
// Reg 0x13 = 0xa0 (ALC Target=-1.5dB, ALC Hold time =0 mS)
{ES8388_ADCCONTROL11, 0xa0},
// Reg 0x14 = 0x12(Decay time =820uS , Attack time = 416 uS)
{ES8388_ADCCONTROL12, 0x12},
// Reg 0x15 = 0x06(ALC mode)
{ES8388_ADCCONTROL13, 0x06},
// Reg 0x16 = 0xc3(nose gate = -40.5dB, NGG = 0x01(mute ADC))
{ES8388_ADCCONTROL14, 0xc3},
// Power on ADC
{ES8388_DACCONTROL21, 0x80},
// Start state machine
{ES8388_CHIPPOWER, 0xF0},
};
static constexpr i2c::I2CRegValue ES8388_UNMUTE[] PROGMEM = {
// unmute ADC with fade in
{ES8388_ADCCONTROL7, 0x60},
// unmute DAC with fade in
{ES8388_DACCONTROL3, 0x20},
// Power on ADC, Enable LIN&RIN, Power off MICBIAS, set int1lp to low power mode
{ES8388_ADCPOWER, 0x09},
};
void ES8388::setup() {
// mute DAC
this->set_mute_state_(true);
ES8388_ERROR_FAILED(this->write_registers(ES8388_INIT));
// I2S worker mode
ES8388_ERROR_FAILED(this->write_byte(ES8388_MASTERMODE, 0x00));
/* Chip Control and Power Management */
ES8388_ERROR_FAILED(this->write_byte(ES8388_CONTROL2, 0x50));
// normal all and power up all
ES8388_ERROR_FAILED(this->write_byte(ES8388_CHIPPOWER, 0x00));
// vmidsel/500k
// EnRef=0,Play&Record Mode,(0x17-both of mic&play)
ES8388_ERROR_FAILED(this->write_byte(ES8388_CONTROL1, 0x12));
// i2s 16 bits
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL1, 0x18));
// sample freq 256
// DACFsMode,SINGLE SPEED; DACFsRatio,256
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL2, 0x02));
// 0x00 audio on LIN1&RIN1, 0x09 LIN2&RIN2
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL16, 0x00));
// only left DAC to left mixer enable 0db
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL17, 0x90));
// only right DAC to right mixer enable 0db
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL20, 0x90));
// set internal ADC and DAC use the same LRCK clock, ADC LRCK as internal LRCK
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL21, 0x80));
// vroi=0 - 1.5k VREF to analog output resistance (default)
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL23, 0x00));
// power down adc and line in
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCPOWER, 0xFF));
//@nightdav
ES8388_ERROR_FAILED(
this->write_byte(ES8388_ADCCONTROL1, 0x00)); // +21dB : recommended value for ALC & voice recording
// set to Mono Right
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL3, 0x02));
// I2S 16 Bits length and I2S serial audio data format
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL4, 0x0d));
// ADCFsMode,singel SPEED,RATIO=256
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL5, 0x02));
// ADC Volume
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL8, 0x00));
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL9, 0x00));
//@nightDav
// ALC Config (as recommended by ES8388 user guide for voice recording)
// Reg 0x12 = 0xe2 (ALC enable, PGA Max. Gain=23.5dB, Min. Gain=0dB)
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL10, 0xe2));
// Reg 0x13 = 0xa0 (ALC Target=-1.5dB, ALC Hold time =0 mS)
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL11, 0xa0));
// Reg 0x14 = 0x12(Decay time =820uS , Attack time = 416 uS)
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL12, 0x12));
// Reg 0x15 = 0x06(ALC mode)
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL13, 0x06));
// Reg 0x16 = 0xc3(nose gate = -40.5dB, NGG = 0x01(mute ADC))
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL14, 0xc3));
// Power on ADC
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL21, 0x80));
// Start state machine
ES8388_ERROR_FAILED(this->write_byte(ES8388_CHIPPOWER, 0xF0));
delay(1);
ES8388_ERROR_FAILED(this->write_byte(ES8388_CHIPPOWER, 0x00));
@@ -101,7 +104,13 @@ void ES8388::setup() {
this->set_mute_state_(false);
ES8388_ERROR_FAILED(this->write_registers(ES8388_UNMUTE));
// unmute ADC with fade in
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCCONTROL7, 0x60));
// unmute DAC with fade in
ES8388_ERROR_FAILED(this->write_byte(ES8388_DACCONTROL3, 0x20));
// Power on ADC, Enable LIN&RIN, Power off MICBIAS, set int1lp to low power mode
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCPOWER, 0x09));
#ifdef USE_ES8388_SELECT
if (this->dac_output_select_ != nullptr) {
@@ -143,8 +152,8 @@ void ES8388::dump_config() {
ESP_LOGCONFIG(TAG, "ES8388 Audio Codec:");
LOG_I2C_DEVICE(this);
#ifdef USE_ES8388_SELECT
LOG_SELECT(" ", "DAC Output", this->dac_output_select_);
LOG_SELECT(" ", "ADC Input Mic", this->adc_input_mic_select_);
LOG_SELECT(" ", "DacOutputSelect", this->dac_output_select_);
LOG_SELECT(" ", "ADCInputMicSelect", this->adc_input_mic_select_);
#endif
}
+5 -9
View File
@@ -24,12 +24,14 @@ ADC_INPUT_MIC_OPTIONS = AdcInputMicLine.options(
)
ADC_INPUT_MIC_OPTION_NAMES = list(ADC_INPUT_MIC_OPTIONS)
DacOutputSelect = es8388_ns.class_("DacOutputSelect", select.Select)
ADCInputMicSelect = es8388_ns.class_("ADCInputMicSelect", select.Select)
CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(CONF_ES8388_ID): cv.use_id(ES8388),
cv.Optional(CONF_DAC_OUTPUT): select.select_schema(
select.ApplySelect,
DacOutputSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP,
).extend(
@@ -40,7 +42,7 @@ CONFIG_SCHEMA = cv.All(
}
),
cv.Optional(CONF_ADC_INPUT_MIC): select.select_schema(
select.ApplySelect,
ADCInputMicSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP,
).extend(
@@ -59,18 +61,12 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_ES8388_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_DAC_OUTPUT,
hub.set_dac_output_select,
options=DAC_OUTPUT_OPTION_NAMES,
apply="set_dac_output(static_cast<es8388::DacOutputLine>(index))",
publish="before",
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=DAC_OUTPUT_OPTION_NAMES
)
await selects(
CONF_ADC_INPUT_MIC,
hub.set_adc_input_mic_select,
options=ADC_INPUT_MIC_OPTION_NAMES,
apply="set_adc_input_mic(static_cast<es8388::AdcInputMicLine>(index))",
publish="before",
)
if (
initial := config.get(CONF_DAC_OUTPUT, {}).get(CONF_INITIAL_OPTION)
@@ -0,0 +1,10 @@
#include "adc_input_mic_select.h"
namespace esphome::es8388 {
void ADCInputMicSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(index));
}
} // namespace esphome::es8388
@@ -0,0 +1,13 @@
#pragma once
#include "esphome/components/es8388/es8388.h"
#include "esphome/components/select/select.h"
namespace esphome::es8388 {
class ADCInputMicSelect final : public select::Select, public Parented<ES8388> {
protected:
void control(size_t index) override;
};
} // namespace esphome::es8388
@@ -0,0 +1,10 @@
#include "dac_output_select.h"
namespace esphome::es8388 {
void DacOutputSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_dac_output(static_cast<DacOutputLine>(index));
}
} // namespace esphome::es8388
@@ -0,0 +1,13 @@
#pragma once
#include "esphome/components/es8388/es8388.h"
#include "esphome/components/select/select.h"
namespace esphome::es8388 {
class DacOutputSelect final : public select::Select, public Parented<ES8388> {
protected:
void control(size_t index) override;
};
} // namespace esphome::es8388
+1 -3
View File
@@ -2389,9 +2389,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.All(
cv.string_strict, cv.Length(min=1)
),
cv.Optional(CONF_SOURCE): cv.git_ref,
cv.Optional(CONF_REF): cv.string,
cv.Optional(CONF_PATH): cv.string,
}
+11 -19
View File
@@ -17,13 +17,8 @@
#include "hal/rtc_io_hal.h"
#endif
// some variants don't map rtcio to gpio 1:1
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32H21)
#define RTCIO_NEED_MAPPING 1 // NOLINT
#endif
#if defined(USE_ESP32_VARIANT_ESP32) || (SOC_RTCIO_PIN_COUNT > 0 && defined(USE_GPIO_HOLD))
#define ISR_PIN_USE_RTC 1 // NOLINT
#ifndef SOC_GPIO_SUPPORT_RTC_INDEPENDENT
#define SOC_GPIO_SUPPORT_RTC_INDEPENDENT 0 // NOLINT
#endif
namespace esphome {
@@ -58,12 +53,10 @@ struct ISRPinArg {
#ifdef USE_GPIO_HOLD
bool hold;
#endif
#ifdef ISR_PIN_USE_RTC
#if defined(USE_ESP32_VARIANT_ESP32)
bool use_rtc;
#ifdef RTCIO_NEED_MAPPING
int rtc_pin;
#endif
#endif
};
#ifdef USE_GPIO_HOLD
@@ -76,15 +69,17 @@ static inline void refresh_hold(const ESP32InternalGPIOPin &pin) {
static inline void IRAM_ATTR isr_refresh_hold(const ISRPinArg *arg) {
if (!arg->hold)
return;
#if defined(ISR_PIN_USE_RTC) && defined(SOC_RTCIO_HOLD_SUPPORTED)
#if defined(USE_ESP32_VARIANT_ESP32)
if (arg->use_rtc) {
#ifdef RTCIO_NEED_MAPPING
rtcio_hal_hold_disable(arg->rtc_pin);
rtcio_hal_hold_enable(arg->rtc_pin);
#else
return;
}
#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
// GPIO0-21 are held from the RTC domain; the digital hold mask is zero for them
if (arg->pin < SOC_RTCIO_PIN_COUNT) {
rtcio_hal_hold_disable(arg->pin);
rtcio_hal_hold_enable(arg->pin);
#endif
return;
}
#endif
@@ -101,12 +96,10 @@ ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
#ifdef USE_GPIO_HOLD
arg->hold = this->get_hold();
#endif
#ifdef ISR_PIN_USE_RTC
#if defined(USE_ESP32_VARIANT_ESP32)
arg->use_rtc = rtc_gpio_is_valid_gpio(this->get_pin_num());
#ifdef RTCIO_NEED_MAPPING
if (arg->use_rtc)
arg->rtc_pin = rtc_io_number_get(this->get_pin_num());
#endif
#endif
return ISRInternalGPIOPin((void *) arg);
}
@@ -258,8 +251,7 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
if (diff & gpio::FLAG_INPUT) {
if (flags & gpio::FLAG_INPUT) {
gpio_hal_input_enable(&GPIO_HAL, arg->pin);
#ifdef USE_ESP32_VARIANT_ESP32
// Only ESP32 uses the RTC registers for pullup/pulldown
#if defined(USE_ESP32_VARIANT_ESP32)
if (arg->use_rtc) {
if (flags & gpio::FLAG_PULLUP) {
rtcio_hal_pullup_enable(arg->rtc_pin);
@@ -114,8 +114,7 @@ BLECharacteristicNotifyAction = esp32_ble_server_automations_ns.class_(
"BLECharacteristicNotifyAction", automation.Action
)
bytebuffer_ns = cg.esphome_ns.namespace("bytebuffer")
# Unscoped in C++; is_class keeps the generated Endian::LITTLE spelling
Endian = bytebuffer_ns.enum("Endian", is_class=True)
Endianness_ns = bytebuffer_ns.namespace("Endian")
PROPERTY_MAP = {
@@ -411,7 +410,10 @@ def value_schema(default_type=UNDEFINED, templatable=True):
),
cv.Optional(CONF_STRING_ENCODING, default="utf_8"): validate_encoding,
cv.Optional(CONF_ENDIANNESS, default="LITTLE"): cv.enum(
Endian.options("LITTLE", "BIG")
{
"LITTLE": Endianness_ns.LITTLE,
"BIG": Endianness_ns.BIG,
}
),
},
validate_value_type,
+17 -13
View File
@@ -54,21 +54,25 @@ CAN_MODES = {
# The supported bit rates differ between ESP32 variants.
# See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI)
CAN_SPEEDS_ESP32 = CanSpeed.options(
"20KBPS",
"25KBPS",
"50KBPS",
"100KBPS",
"125KBPS",
"250KBPS",
"500KBPS",
"800KBPS",
"1000KBPS",
prefix="CAN_",
)
CAN_SPEEDS_ESP32 = {
"20KBPS": CanSpeed.CAN_20KBPS,
"25KBPS": CanSpeed.CAN_25KBPS,
"50KBPS": CanSpeed.CAN_50KBPS,
"100KBPS": CanSpeed.CAN_100KBPS,
"125KBPS": CanSpeed.CAN_125KBPS,
"250KBPS": CanSpeed.CAN_250KBPS,
"500KBPS": CanSpeed.CAN_500KBPS,
"800KBPS": CanSpeed.CAN_800KBPS,
"1000KBPS": CanSpeed.CAN_1000KBPS,
}
CAN_SPEEDS_ESP32_S2 = {
**CanSpeed.options("1KBPS", "5KBPS", "10KBPS", "12K5BPS", "16KBPS", prefix="CAN_"),
"1KBPS": CanSpeed.CAN_1KBPS,
"5KBPS": CanSpeed.CAN_5KBPS,
"10KBPS": CanSpeed.CAN_10KBPS,
"12K5BPS": CanSpeed.CAN_12K5BPS,
"16KBPS": CanSpeed.CAN_16KBPS,
"20KBPS": CanSpeed.CAN_20KBPS,
**CAN_SPEEDS_ESP32,
}
@@ -1,5 +1,4 @@
#include "led_strip.h"
#include <algorithm>
#include <cinttypes>
#ifdef USE_ESP32
@@ -15,8 +14,6 @@ namespace esphome::esp32_rmt_led_strip {
ESPHOME_LOG_TAG(TAG, "esp32_rmt_led_strip");
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
// Idle time the line must stay low between frames so the strip latches
static constexpr uint32_t RESET_GAP_US = 50;
// Query the RMT default clock source frequency. This varies by variant:
// APB (80MHz) on ESP32/S2/S3/C3, PLL_F80M (80MHz) on C6/P4, XTAL (32MHz) on H2.
@@ -94,20 +91,10 @@ void ESP32RMTLEDStripLightOutput::setup() {
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1);
#endif
const uint32_t resolution_hz = rmt_resolution_hz();
// Longest wire time of one frame; write_state() defers instead of blocking until it has elapsed
uint32_t bit_ticks = std::max(this->params_.bit0.duration0 + this->params_.bit0.duration1,
this->params_.bit1.duration0 + this->params_.bit1.duration1);
uint64_t frame_ticks = (uint64_t) bit_ticks * buffer_size * RMT_SYMBOLS_PER_BYTE + this->params_.reset.duration0 +
this->params_.reset.duration1;
this->frame_time_us_ = frame_ticks * 1000000 / resolution_hz + RESET_GAP_US;
// Seed with the interval write_state() gates on, so the first frame is not deferred
this->last_refresh_ = micros() - std::max(this->frame_time_us_, this->max_refresh_rate_);
rmt_tx_channel_config_t channel;
memset(&channel, 0, sizeof(channel));
channel.clk_src = RMT_CLK_SRC_DEFAULT;
channel.resolution_hz = resolution_hz;
channel.resolution_hz = rmt_resolution_hz();
channel.gpio_num = gpio_num_t(this->pin_);
channel.mem_block_symbols = this->rmt_symbols_;
channel.trans_queue_depth = 1;
@@ -170,21 +157,22 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi
}
void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
// Previous frame still on the wire, inside its reset gap, or refreshing too often: try again next loop
if (this->throttle_show_(micros(), this->last_refresh_, std::max(this->frame_time_us_, this->max_refresh_rate_)))
uint32_t now = micros();
if (this->throttle_show_(now, this->last_refresh_, this->max_refresh_rate_))
return;
// The gate above should always cover the previous frame; never overwrite rmt_buf_ while the RMT still reads it
if (rmt_tx_wait_all_done(this->channel_, 0) != ESP_OK) {
ESP_LOGE(TAG, "RMT TX still busy");
this->status_set_warning();
this->schedule_show();
return;
}
this->last_refresh_ = now;
this->mark_shown_();
ESP_LOGVV(TAG, "Writing RGB values to bus");
esp_err_t error = rmt_tx_wait_all_done(this->channel_, 1000);
if (error != ESP_OK) {
ESP_LOGE(TAG, "RMT TX timeout");
this->status_set_warning();
return;
}
delayMicroseconds(50);
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
memcpy(this->rmt_buf_, this->buf_, this->get_buffer_size_());
#else
@@ -215,19 +203,13 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, this->get_buffer_size_(), &config);
error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, this->get_buffer_size_(), &config);
#else
esp_err_t error =
rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config);
error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config);
#endif
if (!this->status_comm_result(error == ESP_OK, nullptr)) {
ESP_LOGE(TAG, "RMT TX error");
// retry next loop iteration so the frame is not lost
this->schedule_show();
return;
}
// Stamped after the transfer started so the gate above stays conservative
this->last_refresh_ = micros();
}
void ESP32RMTLEDStripLightOutput::dump_config() {
@@ -81,8 +81,6 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
uint32_t last_refresh_{0};
uint32_t max_refresh_rate_{0};
// Worst-case wire time of one frame plus the reset gap, computed in setup()
uint32_t frame_time_us_{0};
};
} // namespace esphome::esp32_rmt_led_strip
@@ -9,23 +9,24 @@ from .. import (
CONF_ESPECTRE_ID,
DOMAIN,
ESPectreComponent,
espectre_ns,
supported_traffic_generator_modes,
)
DEPENDENCIES = ["espectre"]
TrafficModeSelect = espectre_ns.class_(
"TrafficModeSelect", select.Select, cg.Parented.template(ESPectreComponent)
)
CONFIG_SCHEMA = select.select_schema(
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
TrafficModeSelect, entity_category=ENTITY_CATEGORY_CONFIG
).extend({cv.GenerateID(CONF_ESPECTRE_ID): cv.use_id(ESPectreComponent)})
async def to_code(config: ConfigType) -> None:
options = supported_traffic_generator_modes(CORE.config[DOMAIN])
hub = await cg.get_variable(config[CONF_ESPECTRE_ID])
await select.sub_selects(config, parent=hub).create(
config,
hub.set_traffic_mode_select,
options=options,
apply="request_traffic_generator_mode(option)",
)
var = await select.new_select(config, options=options)
await cg.register_parented(var, config[CONF_ESPECTRE_ID])
parent = await cg.get_variable(config[CONF_ESPECTRE_ID])
cg.add(parent.set_traffic_mode_select(var))
@@ -0,0 +1,11 @@
#include "espectre_select.h"
#ifdef USE_ESPECTRE
namespace esphome::espectre {
void TrafficModeSelect::control(size_t index) { this->parent_->request_traffic_generator_mode(this->option_at(index)); }
} // namespace esphome::espectre
#endif // USE_ESPECTRE
@@ -0,0 +1,19 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESPECTRE
#include "esphome/components/select/select.h"
#include "../espectre.h"
namespace esphome::espectre {
class TrafficModeSelect final : public select::Select, public Parented<ESPectreComponent> {
protected:
void control(size_t index) override;
};
} // namespace esphome::espectre
#endif // USE_ESPECTRE
+9 -12
View File
@@ -1,14 +1,14 @@
#include "ezo.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
namespace esphome::ezo {
PROGMEM_STRING_TABLE(EzoCommandTypeStrings, "EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION", "EZO_SLOPE",
"EZO_CALIBRATION", "EZO_SLEEP", "EZO_I2C", "EZO_T", "EZO_CUSTOM");
static const char *const EZO_COMMAND_TYPE_STRINGS[] = {"EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION",
"EZO_SLOPE", "EZO_CALIBRATION", "EZO_SLEEP",
"EZO_I2C", "EZO_T", "EZO_CUSTOM"};
PROGMEM_STRING_TABLE(EzoCalibrationTypeStrings, "LOW", "MID", "HIGH");
static const char *const EZO_CALIBRATION_TYPE_STRINGS[] = {"LOW", "MID", "HIGH"};
void EZOSensor::dump_config() {
LOG_SENSOR("", "EZO", this);
@@ -103,8 +103,7 @@ void EZOSensor::loop() {
break;
}
ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1],
LOG_STR_ARG(EzoCommandTypeStrings::get_log_str(to_run->command_type, EzoCommandTypeStrings::LAST_INDEX)));
ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1], EZO_COMMAND_TYPE_STRINGS[to_run->command_type]);
if (buf[0] == 1) {
std::string payload = reinterpret_cast<char *>(&buf[1]);
@@ -169,9 +168,7 @@ void EZOSensor::add_command_(const char *command, EzoCommandType command_type, u
void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) {
// max 21: "Cal,"(4) + type(4) + ","(1) + float(11) + null; use 24 for safety
char payload[24];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("Cal,%s,%0.2f"),
LOG_STR_ARG(EzoCalibrationTypeStrings::get_log_str(type, EzoCalibrationTypeStrings::LAST_INDEX)),
value);
snprintf(payload, sizeof(payload), "Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
@@ -179,7 +176,7 @@ void EZOSensor::set_address(uint8_t address) {
if (address > 0 && address < 128) {
// max 8: "I2C,"(4) + uint8(3) + null
char payload[8];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("I2C,%u"), address);
snprintf(payload, sizeof(payload), "I2C,%u", address);
this->new_address_ = address;
this->add_command_(payload, EzoCommandType::EZO_I2C);
} else {
@@ -200,7 +197,7 @@ void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); }
void EZOSensor::set_t(float value) {
// max 14 bytes: "T,"(2) + float with "%0.2f" (up to 11 chars) + null(1); use 16 for alignment
char payload[16];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("T,%0.2f"), value);
snprintf(payload, sizeof(payload), "T,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_T);
}
@@ -223,7 +220,7 @@ void EZOSensor::set_calibration_point_high(float value) {
void EZOSensor::set_calibration_generic(float value) {
// exact 16 bytes: "Cal," (4) + float with "%0.2f" (up to 11 chars, e.g. "-9999999.99") + null (1) = 16
char payload[16];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("Cal,%0.2f"), value);
snprintf(payload, sizeof(payload), "Cal,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
+12 -1
View File
@@ -34,7 +34,10 @@ void FT63X6Touchscreen::setup() {
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_ANY_EDGE);
}
pulse_reset(this->reset_pin_, {.low_ms = 10});
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->hard_reset_();
}
// Get touch resolution
if (this->x_raw_max_ == this->x_raw_min_) {
@@ -54,6 +57,14 @@ void FT63X6Touchscreen::setup() {
this->write_byte(FT6X36_ADDR_TOUCHRATE_ACTIVE, 0x0E);
}
void FT63X6Touchscreen::hard_reset_() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
}
void FT63X6Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "FT63X6 Touchscreen:");
LOG_I2C_DEVICE(this);
+1
View File
@@ -27,6 +27,7 @@ class FT63X6Touchscreen final : public Touchscreen, public i2c::I2CDevice {
void set_threshold(uint8_t threshold) { this->threshold_ = threshold; }
protected:
void hard_reset_();
void update_touches() override;
InternalGPIOPin *interrupt_pin_{nullptr};
@@ -83,7 +83,13 @@ void GSL3670Touchscreen::update_touches() {
void GSL3670Touchscreen::clear_reg_() {
ESP_LOGD(TAG, "clear_reg");
pulse_reset(this->reset_pin_, {.low_ms = 1, .high_after_ms = 5});
// GPIO reset pulse
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
delay(5);
}
this->write_reg8_(0x88, 0x01);
// delay(5);
@@ -100,7 +106,12 @@ void GSL3670Touchscreen::clear_reg_() {
void GSL3670Touchscreen::reset_() {
ESP_LOGD(TAG, "reset");
pulse_reset(this->reset_pin_, {.low_ms = 1, .high_after_ms = 5});
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
delay(5);
}
this->write_reg8_(0xe4, 0x04);
+17 -9
View File
@@ -3,11 +3,7 @@ import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components import climate, logger, uart
from esphome.components.climate import (
CLIMATE_MODES,
CLIMATE_PRESETS,
CLIMATE_SWING_MODES,
)
from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode
from esphome.components.logger import DOMAIN as LOGGER_DOMAIN
from esphome.components.wifi import DOMAIN as WIFI_DOMAIN
import esphome.config_validation as cv
@@ -81,19 +77,31 @@ AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = AirflowHorizontalDirection.options(
)
SUPPORTED_SWING_MODES_OPTIONS = {
k: CLIMATE_SWING_MODES[k] for k in ("OFF", "VERTICAL", "HORIZONTAL", "BOTH")
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF,
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
}
SUPPORTED_CLIMATE_MODES_OPTIONS = {
k: CLIMATE_MODES[k] for k in ("OFF", "HEAT_COOL", "COOL", "HEAT", "DRY", "FAN_ONLY")
"OFF": ClimateMode.CLIMATE_MODE_OFF, # always available
"HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, # always available
"COOL": ClimateMode.CLIMATE_MODE_COOL,
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
"DRY": ClimateMode.CLIMATE_MODE_DRY,
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
}
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = {
k: CLIMATE_PRESETS[k] for k in ("AWAY", "BOOST", "COMFORT")
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT,
}
SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
k: CLIMATE_PRESETS[k] for k in ("AWAY", "BOOST", "SLEEP")
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
}
HonControlMethod = haier_ns.enum("HonControlMethod", True)
+24 -14
View File
@@ -6,7 +6,6 @@
#include "esphome/components/wifi/wifi_component.h"
#endif
#include "haier_base.h"
#include "esphome/core/progmem.h"
using namespace esphome::climate;
using namespace esphome::uart;
@@ -20,16 +19,28 @@ constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400;
PROGMEM_STRING_TABLE(ProtocolPhaseStrings, "SENDING_INIT_1", "SENDING_INIT_2", "SENDING_FIRST_STATUS_REQUEST",
"SENDING_FIRST_ALARM_STATUS_REQUEST", "IDLE", "SENDING_STATUS_REQUEST",
"SENDING_UPDATE_SIGNAL_REQUEST", "SENDING_SIGNAL_LEVEL", "SENDING_CONTROL",
"SENDING_ACTION_COMMAND", "SENDING_ALARM_STATUS_REQUEST", "UNKNOWN");
const LogString *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
static_assert(ProtocolPhaseStrings::COUNT == ((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1,
"ProtocolPhaseStrings must match the enum ProtocolPhases, with UNKNOWN last");
// UNKNOWN (-1) wraps past the table and reads as the last entry, "UNKNOWN"
return ProtocolPhaseStrings::get_log_str(static_cast<uint8_t>(phase), ProtocolPhaseStrings::LAST_INDEX);
const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
static const char *phase_names[] = {
"SENDING_INIT_1",
"SENDING_INIT_2",
"SENDING_FIRST_STATUS_REQUEST",
"SENDING_FIRST_ALARM_STATUS_REQUEST",
"IDLE",
"SENDING_STATUS_REQUEST",
"SENDING_UPDATE_SIGNAL_REQUEST",
"SENDING_SIGNAL_LEVEL",
"SENDING_CONTROL",
"SENDING_ACTION_COMMAND",
"SENDING_ALARM_STATUS_REQUEST",
"UNKNOWN" // Should be the last!
};
static_assert(
(sizeof(phase_names) / sizeof(char *)) == (((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1),
"Wrong phase_names array size. Please, make sure that this array is aligned with the enum ProtocolPhases");
int phase_index = (int) phase;
if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
return phase_names[phase_index];
}
bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
@@ -60,8 +71,7 @@ HaierClimateBase::~HaierClimateBase() {}
void HaierClimateBase::set_phase(ProtocolPhases phase) {
if (this->protocol_phase_ != phase) {
ESP_LOGV(TAG, "Phase transition: %s => %s", LOG_STR_ARG(phase_to_string_(this->protocol_phase_)),
LOG_STR_ARG(phase_to_string_(phase)));
ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase));
this->protocol_phase_ = phase;
}
}
@@ -215,7 +225,7 @@ haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_hand
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(haier_protocol::FrameType request_type) {
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) request_type,
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)));
phase_to_string_(this->protocol_phase_));
if (this->protocol_phase_ > ProtocolPhases::IDLE) {
this->set_phase(ProtocolPhases::IDLE);
} else {
+1 -1
View File
@@ -95,7 +95,7 @@ class HaierClimateBase : public esphome::Component,
SENDING_ALARM_STATUS_REQUEST,
NUM_PROTOCOL_PHASES
};
const LogString *phase_to_string_(ProtocolPhases phase);
const char *phase_to_string_(ProtocolPhases phase);
virtual void set_handlers() = 0;
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming)
+1 -1
View File
@@ -564,7 +564,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
default:
// Shouldn't get here
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)), (int) this->protocol_phase_);
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
this->set_phase(ProtocolPhases::SENDING_INIT_1);
break;
}
+5 -6
View File
@@ -9,9 +9,12 @@ from ..climate import (
CONF_HORIZONTAL_AIRFLOW,
CONF_VERTICAL_AIRFLOW,
HonClimate,
haier_ns,
)
CODEOWNERS = ["@paveldn"]
VerticalAirflowSelect = haier_ns.class_("VerticalAirflowSelect", select.Select)
HorizontalAirflowSelect = haier_ns.class_("HorizontalAirflowSelect", select.Select)
# Same order as VERTICAL_SWING_MODES_ORDER and HORIZONTAL_SWING_MODES_ORDER in hon_climate.h
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = [
@@ -36,12 +39,12 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.Optional(CONF_VERTICAL_AIRFLOW): select.select_schema(
select.ApplySelect,
VerticalAirflowSelect,
icon="mdi:arrow-expand-vertical",
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_HORIZONTAL_AIRFLOW): select.select_schema(
select.ApplySelect,
HorizontalAirflowSelect,
icon="mdi:arrow-expand-horizontal",
entity_category=ENTITY_CATEGORY_CONFIG,
),
@@ -56,13 +59,9 @@ async def to_code(config: ConfigType) -> None:
CONF_VERTICAL_AIRFLOW,
hub.set_vertical_airflow_select,
options=AIRFLOW_VERTICAL_DIRECTION_OPTIONS,
apply="set_vertical_airflow("
"haier::airflow_order_at(haier::VERTICAL_SWING_MODES_ORDER, index))",
)
await selects(
CONF_HORIZONTAL_AIRFLOW,
hub.set_horizontal_airflow_select,
options=AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS,
apply="set_horizontal_airflow("
"haier::airflow_order_at(haier::HORIZONTAL_SWING_MODES_ORDER, index))",
)
@@ -0,0 +1,15 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../hon_climate.h"
namespace esphome::haier {
class HorizontalAirflowSelect : public select::Select, public Parented<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_horizontal_airflow(airflow_order_at(HORIZONTAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
@@ -0,0 +1,15 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../hon_climate.h"
namespace esphome::haier {
class VerticalAirflowSelect : public select::Select, public Parented<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_vertical_airflow(airflow_order_at(VERTICAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
@@ -93,7 +93,7 @@ haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cyc
if (this->protocol_phase_ >= ProtocolPhases::IDLE)
return HaierClimateBase::timeout_default_handler_(message_type);
ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)));
phase_to_string_(this->protocol_phase_));
ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
if (new_phase >= ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST)
new_phase = ProtocolPhases::SENDING_INIT_1;
@@ -217,7 +217,7 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
default:
// Shouldn't get here
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
LOG_STR_ARG(phase_to_string_(this->protocol_phase_)), (int) this->protocol_phase_);
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
this->set_phase(ProtocolPhases::SENDING_INIT_1);
break;
}
@@ -289,12 +289,8 @@ using socket_t = int;
#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_OSX
// The precompiled header brings in ArduinoJson.h, whose `using namespace ArduinoJson` makes MacTypes.h's
// global Ptr ambiguous with ArduinoJson's Ptr class; give the macOS one another name here
#define Ptr esphome_mactypes_Ptr
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
#undef Ptr
#endif // TARGET_OS_OSX
#endif // _WIN32
+6 -3
View File
@@ -1,7 +1,6 @@
#include "htu21d.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome::htu21d {
@@ -113,7 +112,11 @@ void HTU21DComponent::set_heater(bool status) {
this->status_set_warning();
return;
}
assign_bits(raw_heater, 1 << HTU21D_REG_HTRE_BIT, status);
if (status) {
raw_heater |= (1 << HTU21D_REG_HTRE_BIT);
} else {
raw_heater &= ~(1 << HTU21D_REG_HTRE_BIT);
}
if (this->write_register(HTU21D_WRITERHT_REG_CMD, &raw_heater, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
+4 -1
View File
@@ -40,7 +40,10 @@ RG_MODELS = {
# 1.200 - https://rainsensors.com/wp-content/uploads/sites/3/2022/03/2022.02.17-rev-1.200-rg-9_instructions.pdf
}
RG15_RESOLUTION = RG15Resolution.options("low", "high", prefix="FORCE_")
RG15_RESOLUTION = {
"low": RG15Resolution.FORCE_LOW,
"high": RG15Resolution.FORCE_HIGH,
}
SUPPORTED_OPTIONS = {
CONF_ACC: ["RG_15"],
@@ -270,23 +270,21 @@ void I2SAudioMicrophone::mic_task(void *params) {
{ // Ensures the samples vector is freed when the task stops
// Copied so the format cannot change while the task runs
const audio::AudioStreamInfo stream_info = this_microphone->audio_stream_info_;
const size_t bytes_to_read = stream_info.ms_to_bytes(READ_DURATION_MS);
const size_t bytes_to_read = this_microphone->audio_stream_info_.ms_to_bytes(READ_DURATION_MS);
std::vector<uint8_t> samples;
samples.reserve(bytes_to_read);
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING);
while (!(xEventGroupGetBits(this_microphone->event_group_) & MicrophoneEventGroupBits::COMMAND_STOP)) {
if (this_microphone->has_data_callbacks_()) {
if (this_microphone->data_callbacks_.size() > 0) {
samples.resize(bytes_to_read);
size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * READ_DURATION_MS);
samples.resize(bytes_read);
if (this_microphone->correct_dc_offset_) {
this_microphone->fix_dc_offset_(samples);
}
this_microphone->publish_audio_(samples, stream_info);
this_microphone->data_callbacks_.call(samples);
} else {
vTaskDelay(pdMS_TO_TICKS(READ_DURATION_MS));
}
+6 -1
View File
@@ -19,7 +19,12 @@ void ICNT86Touchscreen::setup() {
}
// Perform reset if necessary
pulse_reset(this->reset_pin_, {.low_ms = 10});
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = this->display_->get_native_width();
+3 -4
View File
@@ -1,7 +1,8 @@
"""Shared base for the infrared and radio_frequency entity components."""
import esphome.codegen as cg
from esphome.const import CONF_API
from esphome.components import remote_base
from esphome.components.api import DOMAIN as API_DOMAIN
from esphome.core import CORE
from esphome.core.entity_helpers import queue_entity_register
from esphome.types import ConfigType
@@ -23,10 +24,8 @@ async def attach_transmitter(var: cg.MockObj, config: ConfigType, key: str) -> N
does. Without this call anywhere in the build a request is answered as
soon as the frame is handed over.
"""
from esphome.components import remote_base
await remote_base.register_transmittable(var, config, key)
if CONF_API in CORE.config:
if API_DOMAIN in CORE.config:
cg.add_define("USE_IR_RF_TRANSMIT_COMPLETE")
@@ -2,20 +2,11 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::kamstrup_kmp {
ESPHOME_LOG_TAG(TAG, "kamstrup_kmp");
// KMP units indexed by unit code; the empty first entry doubles as the fallback for unknown codes
PROGMEM_STRING_TABLE(KmpUnits, "", "Wh", "kWh", "MWh", "GWh", "J", "kJ", "MJ", "GJ", "Cal", "kCal", "Mcal", "Gcal",
"varh", "kvarh", "Mvarh", "Gvarh", "VAh", "kVAh", "MVAh", "GVAh", "kW", "kW", "MW", "GW", "kvar",
"kvar", "Mvar", "Gvar", "VA", "kVA", "MVA", "GVA", "V", "A", "kV", "kA", "C", "K", "l", "m3",
"l/h", "m3/h", "m3xC", "ton", "ton/h", "h", "hh:mm:ss", "yy:mm:dd", "yyyy:mm:dd", "mm:dd", "",
"bar", "RTC", "ASCII", "m3 x 10", "ton x 10", "GJ x 10", "minutes", "Bitfield", "s", "ms", "days",
"RTC-Q", "Datetime");
void KamstrupKMPComponent::dump_config() {
ESP_LOGCONFIG(TAG, "kamstrup_kmp:");
LOG_UPDATE_INTERVAL(this);
@@ -246,6 +237,8 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_
}
void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint8_t unit_idx) {
const char *unit = unit_idx < sizeof(UNITS) / sizeof(UNITS[0]) ? UNITS[unit_idx] : "";
// Standard sensors
if (command == CMD_HEAT_ENERGY && this->heat_energy_sensor_ != nullptr) {
this->heat_energy_sensor_->publish_state(value);
@@ -270,8 +263,7 @@ void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint
}
}
ESP_LOGD(TAG, "Received value for command 0x%04X: %.3f [%s]", command, value,
LOG_STR_ARG(KmpUnits::get_log_str(unit_idx, 0)));
ESP_LOGD(TAG, "Received value for command 0x%04X: %.3f [%s]", command, value, unit);
}
} // namespace esphome::kamstrup_kmp
Loaded 100 of 357 files, more files were not shown because too many files have changed in this diff. Show more