_run_idedata raises EsphomeError() with no message, so the warning
shows 'unavailable ()' which looks like a bug. Use a generic fallback
explanation when str(exc) is empty.
_decode_pc shells out to PlatformIO via _run_idedata; without a
populated build dir for the device that subprocess fails for every
PC/BT line in a crash dump. Disable decoding after the first
EsphomeError (per logs session) and emit a single user-facing warning
instead of retrying on every line.
Also rename the helper to _LogLineProcessor since it now owns the
per-session decode-enabled state, not just the decode call.
When 'esphome logs' processes a crash backtrace from the device,
process_stacktrace -> _decode_pc -> _run_idedata can raise
EsphomeError if the local build dir hasn't been populated (e.g. the
device was flashed from a different machine). on_log runs inside an
asyncio protocol callback, so the unhandled exception triggers
'Fatal error: protocol.data_received() call failed.', the loop tears
the connection down, and ReconnectLogic immediately reconnects. The
device replays the same crash trace and we loop forever.
Wrap the per-line decode in a helper that swallows EsphomeError so
the connection stays up. Also covered with unit tests for the new
helper.
The generated src/main.cpp.o was being skipped by _scan_source_symbols
(which only walked src/esphome/) and _source_file_to_component had no
rule for it. As a result setup()/loop() fell through to the
"app_framework" heuristic in const.py instead of being counted as
[esphome]core, both via the nm scan path and the linker map path.
Scan all of src/ and recognize .../src/main.cpp.o as core.
Replace `type(mock_response).content = property(...)` with a tiny
_BodyReadErrorResponse stub class. The previous form mutates
MagicMock's class globally, which can leak into every other
MagicMock-using test in this file.
`requests.Response.content` reads the body lazily, so chunked-decode,
gzip-decode, and mid-stream connection drops all surface as
RequestException subclasses on first access -- not from requests.get
itself. The previous code accessed `.content` outside the surrounding
try/except, so any of those (rare but real) errors would propagate
out of download_content instead of falling back to the cached file
or raising the user-friendly cv.Invalid.
Move `data = req.content` inside the try block so the existing error
path handles it. Two new tests cover the with-cache and no-cache
branches using a `ChunkedEncodingError` injected on `.content`.
- _read_etag collapses into a single try/except that handles both stat
and read failures (and the unlink) uniformly.
- _mtime_seconds() helper compares mtimes at whole-second resolution
instead of nanoseconds. Sub-second precision varies by filesystem
(FAT does 2s, NTFS 100ns, APFS/ext4 ns); whole seconds is the only
resolution every supported filesystem can preserve through a
set+read round-trip.
- has_remote_file_changed: walrus-assign `etag` and `new_etag` instead
of an explicit binding + immediate check.
- New test fixtures `mock_requests_head`, `mock_requests_get`,
`mock_has_remote_file_changed`, `mock_write_file` replace the
per-test `@patch(...)` decorator stacks. Tests are simpler and the
fixtures can be reused by future tests in the file.
- Both audio_file and speaker.media_player now use functools.partial to
bind path_for=_compute_local_file_path instead of an inline lambda.
- download_content_many's dedup loop becomes a dict comprehension.
Last-URL wins now (dict-comp semantics) instead of first-URL; in
practice duplicate paths only arise when the URL itself is duplicated,
so the choice is meaningless. Test updated accordingly.
- download_content_many now de-duplicates by `path` so two callers
asking for the same cache file (e.g. the same URL referenced twice
in a config) can't race on download_content's non-atomic write. When
duplicates are present, the first-seen URL for that path wins.
- Clamp `max_workers` to at least 1 so an invalid caller value can't
raise ValueError out of ThreadPoolExecutor.
- _write_etag: suppress OSError (not just FileNotFoundError) when removing
a stale sidecar -- ETag persistence is best-effort and a permission
error there must not abort an otherwise-successful download.
- Atomic-write test: patch write_file directly so the failure happens
inside the helper that does the rename, instead of before
download_content even calls it.