Stream device logs over the web_server /events Server-Sent Events feed so
'esphome logs' works on devices that have web_server: but no api:. This is
the logging counterpart to web_server OTA. Priority stays API, then MQTT,
then web_server. Reconnects automatically when the stream drops.
Factor the resolve-to-URLs step and the web_server port/auth lookup shared
with web_server OTA into a new web_server_helpers module (resolve_web_server_urls
and get_web_server_connection), with helpers.format_ip_url for IPv4/IPv6 URL
formatting, and broaden the missing-transport log error to suggest web_server:
alongside api:/MQTT/USB.
A device with a static IP, mDNS disabled, and no api: component failed
logs with "All specified devices ['OTA'] could not be resolved" and a
hint to set use_address; the hint is misleading since the static IP
already resolves, the real gap is that network logs ride the native API.
Name the missing transport instead: api: for logs, an ota: platform for
uploads. The generic "could not be resolved" message stays for a
genuinely unreachable address.
The unanchored leading \w* could match fields like 'monkey:' (via the
'key' fragment), naming a non-sensitive field in the deprecation
warning. Restrict the fragment to either start the name or follow '_',
so warnings only fire when there's an actual sensitive-shaped field
the author can migrate.
Address reviewer feedback:
- legacy regex was wrapping password: !secret name and clobbering
the dumper's user-friendly !secret round-trip; extend the negative
lookahead to skip !secret (and !lambda) values entirely
- drop the in-replacement !lambda check now that the lookahead handles it
- reword the thread-safety claim in dump() since _SECRET_VALUES /
_SECRET_CACHE remain module globals
- reword the SensitiveStr representer registration comment to reflect
PyYAML's MRO-walked dispatch rather than registration order
- tighten the legacy regex comment
Adds tests for the !secret and !lambda skip paths.
Lambda values in cv.sensitive(cv.templatable(...)) fields still hit the
legacy regex because Lambda isn't a str and therefore doesn't carry the
SensitiveStr tag. The field IS tagged correctly; warning the author to
add cv.sensitive would be misleading. Still wrap the first line so the
user-visible output matches the prior regex.
Add two tests that exercise command_config end-to-end: one confirms the
legacy fallback wraps an unmarked sensitive field, the other confirms
--show-secrets bypasses redaction. Closes the patch-coverage gap on the
'output = _redact_with_legacy_fallback(output)' line.
The trailing \w* after the fragment over-matched fields like
key_value_pair: which the prior regex did not catch. Drop it so the
fragment must end the captured field name, preserving the previous
matching scope while still capturing the full field name (via the
leading \w*) for the warning message.
Restore the substring regex as a second pass in command_config so
sensitive-shaped fields that haven't been tagged with cv.sensitive(...)
yet are still redacted. Each unique unmarked field name caught by the
heuristic emits a one-time deprecation warning naming the field and the
fix; the fallback itself is slated for removal in 2026.12.0.
cv.sensitive(...) now returns a SensitiveStr (thin str subclass) so the
tag travels with the validated value. yaml_util.dump constructs a
per-call ESPHomeDumper subclass with a class-attribute redaction flag;
the PyYAML representer for SensitiveStr renders values wrapped in
literal \\033[8m...\\033[28m text when show_secrets is False and raw
when True. The post-dump regex in command_config is deleted.
Also tags wifi.ssid sites with cv.sensitive so SSID coverage isn't lost
when the regex (which matched 'ssid:' via substring) goes away.
No module-level mutable state; the per-call subclass keeps each dump
invocation self-contained and thread-safe by construction.